Skip to content

Polymorphic Associations

Brian Riley edited this page Aug 11, 2021 · 1 revision

The DMPRoadmap system makes use of polymorphic associations in several instances. Polymorphic associations are a way for the system to have a single table store that holds data that potentially belongs to different models.

For example our identifiers table stores identifiers for many different models like Plan, User, Org, etc. In earlier versions of Rails you would model this in the database as separate identifier tables, something like:

CREATE TABLE `plan_identifiers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `value` varchar(255) DEFAULT NULL,
  `type` int(11) DEFAULT NULL,
  `plan_id` int(11) NOT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_contributors_on_plan_id` (`plan_id`)
) 

The above works well, but from a Rails standpoint we're not able to effectively share logic between the identifiers due to the way models are defined and have a one-one relationship with their underlying tables. If you're coming from a Java (or other statically typed language) you would use class hierarchies and perhaps an interface here to share logic between these identifier tables.

Rails has an alternate solution called polymorphic associations that we are now using in several places to share logic and reduce our number of similar tables. We do this by defining a single table called identifiers that has a two-column foreign key that Rails understands. This foreign key consists of the id of the record in the other table (identifiable_id) and the class/model name (identifiable_type). For example: SELECT * FROM identifiers WHERE identifiable_id = 1 AND identifiable_type = 'Plan';

Once the polymorphic table has been created, we can then do the following:

class Identifier < ApplicationRecord
  belongs_to :identifiable, polymorphic: true

  # Our single place for logic about an identifier
end

class Plan < ApplicationRecord
  has_many :identifiers, as: :identifiable, dependent: :destroy
end

class User < ApplicationRecord
  has_many :identifiers, as: :identifiable, dependent: :destroy
end

class Org < ApplicationRecord
  has_many :identifiers, as: :identifiable, dependent: :destroy
end

We can then interact with each model's identifiers in the same manner plan.identifiers << Identifier.new(value: 123, type: "foo") or Identifier.new(identifiable: plan, value: 123, type: "foo")

The above works, but for the sake of being DRY we instead introduce a Rails Concern called Identifiable that our Plan, User and Org can include that provides use with a way to share logic about how those models interact with their identifiers.

module Identifiable
  extend ActiveSupport::Concern

  included do
    has_many :identifiers, as: :identifiable, dependent: :destroy

    accepts_nested_attributes_for :identifiers

    # Helper method that lets us get the identifier for the specified IdentifierScheme
    def identifier_for_scheme(scheme:)
      scheme = IdentifierScheme.by_name(scheme.downcase).first if scheme.is_a?(String)
      identifiers.select { |id| id.identifier_scheme == scheme }.last
    end
  end

end

class User < ApplicationRecord
  include Identifiable
end

With the above we can then do user.identifier_for_scheme(scheme: "orcid") to retrieve their ORCID id from the identifiers table.

For an example of how these are used in the code see: