Nissrine Canina
Posted on October 1, 2022
The Ruby on Rails documentation defines polymorphic associations (PA) as: "A slightly more advanced twist on associations is the polymorphic association. With polymorphic associations, a model can belong to more than one other model, on a single association.". As I am researching this topic to implement it in my project, I have found that understanding what problems polymorphic associations solve makes learning this new concept much simpler.
Another way to rephrase the Rails documentation definition is that PA is needed when multiple parent models use one child model. To understand how useful PA is in this case, we should investigate the issues that will arise without applying PAs. For demonstration purposes, let's create a simple Rails application with four models using conventional has_many
and belongs_to
associations.
Then, let's check our comments in the schema:
As we can see in the comments schema, event_id
, post_id
, and article_id
are required even though the comment is meant to be assigned for one model at a time and not all in one instance. Let's hop on our console and see the flow of events:
Note that the comment did not save to the database because it requires the existence of the event and the article (recall the null: false
property in the schema which means that all foreign keys must exist in order to create and save a comment to the database). There is a way around this issue even though it is definitely inefficient:
It is clear how we had to build new columns in our comment table for the event and article when the main purpose was to create a comment for the post only. We can see how conventional associations were not helping the DRY principle (unnecessary rows in tables with null values) in this case. We can remove the constraint null: false
and add optional:true
in the comment class. This will allow us to create and save a comment successfully without manually assigning the unwanted foreign keys; however, they will be still parsed as nil in the comment table.
The efficient solution for this example is PA. Let's update the migration to add PA and remove all the foreign keys for the article, event, and post models.
Then, we update all the models as shown below:
The :commentable
refers to the post, event, and article models. Let's comment on a post and an event in the console and pay close attention to commentable_type
and commentable_id
:
Here we are creating a comment on a post using PA without wasting space in the database. We also follow the DRY principle by removing three belongs_to
associations from the comments model. Also, we are able to link the behavior expected for a commentable component to any model. We are separating concerns and reusing code. Let's create another comment on an event and note the change in the commentable-type
:
We can see that the comments were created without loading our tables with nil columns. The commentable_type
refers to the target model that is associated with the comment. Therefore, only the target model foreign key is created.
The comments are accessible using has_many
association:
It is important to know that accessing the parent model of a specific comment will require the use of .commentable
method. For instance we cannot use comment.post
or comment.event
to access the parent model otherwise we will get a NoMethodError
. Remember that we modified the belongs_to
associations with as: :commentable
. Where commentable refers to the parents' models. Check the console below:
Finally, we can think of PA anytime we are faced with the need to share a model property among other models. For instance, an image model can be used by other models such as products and categories. so, imageable is the way to go!
I hope this walkthrough example is helpful for anyone learning PA for the first time.
Posted on October 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.