-
Notifications
You must be signed in to change notification settings - Fork 109
Polymorphic Associations
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:
- Home
- About
- Development roadmap
- Releases
- Themes
- Google Analytics
- Get involved
- Translations
- Developer guide