Mighty Polymorphic Tables
Untangling the polymorphic association, and a teeny tiny bit about Active Storage
One of the beautiful things about the Rails framework is the built-in relationships given to us by Active Record. Active Record is a gem that provides Rails’ Object-Relational-Mapping (ORM) functionality. This allows our different models to talk to each other via their relationships by setting them up with the appropriate foreign keys and adding associations like belongs_to
and has_many
in their class definitions. For example, let’s say we have a message board app with a User
model and Post
model, where a post is written by one user, but an individual user may have many posts.
class User
has_many :posts
endclass Post
belongs_to :user
end
The table housing the Post
objects will contain a foreign key for an instance of User
that it “belongs” to. (The foreign key can get into the table in any number of ways, whether manually or generated by Rails.) With these macros in place, we can easily find an array of a user’s posts by calling user_instance.posts
! Very convenient.
The relationships make some intuitive sense (in English), and are explained in the Ruby on Rails Guide for Active Record Associations. They are: belongs_to
, has_one
, has_many
, has_many :through
, has_one :through
, and has_and_belongs_to_many
. (That last one looks like a mouthful, but if you understand join tables and has_many :through
, you’re golden!) The guide also provides for guidance on choosing between the similar ones. But if you keep going down the page you will see…
Polymorphic Associations
It all made sense until now. Things had other things, or belonged to something. Well, that is still the case in polymorphic relationships, but with a twist. Let’s read on:
“With polymorphic associations, a model can belong to more than one other model, on a single association.”
Okay, helpful. But let’s see this in action!
The example from Rails Guides shows one way you might set up a Picture
class definition, whose instances may belong to either an Employee
or Product
.
The belongs_to
and has_many
macros are familiar, but notice that Picture
doesn’t seem to directly belong to either Employee
or Product
, and they are linked via imageable
. (The naming convention for the alias of a polymorphic table ends in -able
. However, you can set this name to what works best for you and put them into the migration tables and class definitions!) If there were only one model that was associated with a picture, say, an employee, the pictures table would contain a foreign key employee_id
, which would correspond to the primary key of the Employee
instance it belongs to. However, since both employees and products can have pictures, this is replaced by an imageable_type
and imageable_id
, corresponding to the model name (either employee or product) and primary key, respectively. Pretty clever!
One alternative to using the polymorphic approach in this situation would be to use a single table inheritance (STI) model to create two separate tables for pictures, perhaps EmployeePicture
and ProductPicture
. But using a polymorphic table allows for one multi-purposePicture
table.
There are pros and cons to using a polymorphic association over a STI model, with one important drawback being a loss in data integrity without a strict foreign key column and less isolated model relationships. However, it does have a big advantage in flexibility if you expect to expand.
As an example, imagine we have a dog shelter. A person can adopt many dogs. Right now, people just sign some paperwork, pay a fee, and adopt the puppo. This can be achieved in two models, with the adoption information stored on the Dog
table.
But maybe we decide to up our services a bit. The adoption comes with a follow-up consultation, a free vet visit, specific paperwork depending on the dog, and more detailed information that start to add a lot of columns to the Dog
table. It begins to make sense on the backend for the adoptions to have their own methods and attributes. So we add an Adoption
model and set up our relationships so that people are associated with adoptions, which are in turn tied to a specific dog.
As our reputation and great adoption practices become more well known, we decide to branch out to adopting out cats.
To accomodate our new friends, we could create a more general Animal
table, with columns like “name”, “color”, and “age” that apply to both dogs and cats, but also with additional columns that only apply to one or the other, such as a cat’s FIV status or how often a dog should be walked (which is all the time because they are a good dog yes they are). This works, but there are many cells left empty if they are not applicable (and who wants null
or N/A cells everywhere), and we run back into the problem of an over-bloated table, like when the Dog
model held the adoption data. Plus, you might want to branch out to more animals and this would only get worse.
This is when you may choose to use a polymorphic relationship for the Adoption
model!
Now you can accomodate as many other types of animals that may come your way, storing key attributes and methods for them, and have a single type of adoption record that can fit all of them.
Whether or not you choose to use a polymorphic association is a matter of design choice, but if you believe your domain may grow in a way that might cause multiple models to have a similar relationship to the same model, it’s a good option to consider!
Just a little bit about Active Storage
Active Storage is another great built-in part of Rails, which allows a easy file upload for a user to the cloud (or your local disk for development), while also attaching the file to an instance of a model, such as a picture for a profile or sales listing. The feature is extremely easy to use and set-up, and I recommend giving it a shot.
When you install and add Active Storage to your app, it creates two tables: active_storage_blobs
and active_storage_attachments
. The second table is, you guessed it, a polymorphic table!
It is a join table with columns for a “record_type”, “record_id”, and a “blob_id”. (A BLOB
is a most excellent data type that even has subtypes like TINYBLOB
, MEDIUMBLOB
, and LONGBLOB
. I know, right? Almost makes databases seem cute.)
Anyway, “polymorphic” might sound less intuitive than the rest of the Active Record associations naming scheme, but they’re a small modification that can make a big difference. And then you can save all the animals.
Is anything about this not quite right? Have any questions? Let me know! Just like all of you, I’m a programmer on a journey. (And very early on that journey at that.)
Additional resources: