With polymorphic association, a model can belongs_to several models with a single association.
As an example, let’s think of a e-commerce site, both individuals and groups can make an order to buy products. These individuals and groups are not related (other than both being a type of User, maybe), and so they have different data.
Without polymorphic associations, we would have something like this
1 2 3 4 5 6 7 8 9 10 11 12
classOrder belongs_to :person belongs_to :group end
classPerson has_many :orders end
classGroup has_many :orders end
So, the Order table would have to competing foreign keys: group_id and person_id . This can be a problem, for example, when trying to find the owner of an order, we would have to make a point to check both columns to find correct foreign key, rather than relying on one.
This is a polymorphic association addresses this issue by condensing this functionality into one association. But it’s not easy to correctly represent the association in a relational database. There are four different ways:
The Join tables.
The Type column.
Reverse belongs to.
Exclusive arc.
The Type Column
A simple approach to connect an order to a resource is to use two columns on the order table: resource_type and resource_id . This approach was popularized by Ruby on Rails.
# SQL query # select * # from orders # where owner_type='Order' and owner_id=1;
The Rails convention for naming a polymorphic association use “-able”. This makes it clear in your relationship which class is polymorphic. But you can use whatever name that you like.
Warning
Polymorphic associations come with one huge red flag: compromised data integrity
In a normal belongs_to relationship, we use foreign keys for reference in an association.
They have more power than just forming a link, though. Foreign keys also prevent referential errors by requiring that the object referenced in the foreign table does, in fact, exist. If someone tries to create an object with a foreign key that references a null object, they will get an error.
Unfortunately, polymorphic classes can’t have foreign keys. We use the resource_type and resource_id columns in place of a foreign key. This means we lose the protection that foreign keys offer.
Rails and ActiveRecord help us out on the surface, but anyone with direct access to the database can create or update objects that reference null objects.
PROS
Easy to scale number of models: more models can be easily associated with the polymorphic class.
Follow the DRY principle, creates one class that can be used by many other classes.
CONS
More tables can make querying more difficult and expensive as the data grows.
Cannot have foreign key.
Your data integrity is compromised.
The Join tables
In this approach, we don’t create foreign keys on either of the tables that we want to relate together. Instead, we create a join tables to connect them together.
We’re using foreign key constraints, so the database can ensure that any connection between an order and a resource (group/person) is valid.
Cons
There is no way to require that the resource (group/person) has an order. And it does not enforce uniqueness across the two owners (e.g one order could be incorrectly connected to both a person and a group).
1 2 3 4 5 6
order4 = Order.create(order_ref:"Or004") person = Person.first group = Group.first
We know an order logically belongs to the person or group, but these relationships can be reversed. Instead of having the foreign key on the orders table, we will adding an order_id to people and groups tables.
To ensure that an order belongs to exactly one resource (group/person) at anytime, we need to do something. That’s why this way is called Exclusive Arc.
So, technically, there are two ways to enforce the exclusive constraint.
classAddCheckConstraintToOrder < ActiveRecord::Migration[6.0] defchange execute <<-SQL ALTER TABLE orders ADD CONSTRAINT order_owner_check CHECK ( ( (group_id is not null)::integer + (person_id is not null)::integer ) = 1 ); SQL end end
classPerson < ApplicationRecord has_many :orders end
classGroup < ApplicationRecord has_many :orders end
classOrder < ApplicationRecord belongs_to :group, optional:true belongs_to :person, optional:true end
# Seed person = Person.create(name:"John Doe") group = Group.create(group_name:"Jungles")
Ensure that an order belongs to exactly one resource (group/person) at any type.
An order can’t be orphaned.
Cons
Take a little extra work to setup
It can get a little cumbersome if you have more than three resources.
Conclusion
Due to lack of data integrity guarantees, the Type Column should not be used. The only advantage it has is an ORM such as ActiveRecord may take it very easy to use
The Join tables approach is an improvement over polymorphic joins, but it requires creating extra table for each relationship.
The Reverse Belongs-To models approach has few critical cons.
So, the final approach, the Exclusive Arc. Data integrity is maintained via the check constraint.
However, there are few concerns:
Multiple null fields. But in the case of PostgreSQL, null values are almost free.
Adding a new table requires adding a column to the exclusive belongs-to table. If this was a large, heavily used table there might be an issue with how long the table would be locked. With PostgreSQL, this is not a problem. Nullable fields can be added quickly regardless of table size. The updated check constraint can also be added without blocking concurrent usage.
In conclusion, I suggest using an Exclusive Arc approach to represent a polymorphic association.