Mighty Polymorphic Tables

Untangling the polymorphic association, and a teeny tiny bit about Active Storage

Photo by Federico Beccari on Unsplash

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.

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

animated gif of the Mighty Morphin Power Rangers
animated gif of the Mighty Morphin Power Rangers
Copyright law on gifs is iffy, but I had to do it.

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.

ERD showing a Person model with a has many relationship to a Dog model
ERD showing a Person model with a has many relationship to a Dog model

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.

ERD showing a Person class joined to a Dog class by an Adoption class
ERD showing a Person class joined to a Dog class by an Adoption class

As our reputation and great adoption practices become more well known, we decide to branch out to adopting out cats.

photo of a black cat on a cat bed, with a blue bow on top
photo of a black cat on a cat bed, with a blue bow on top
This little treasure and more are actually adoptable from King Street Cats in the DC area

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.

ERD showing a Person model linked by an Adoption model to several other animal models in a polymorphic association
ERD showing a Person model linked by an Adoption model to several other animal models in a polymorphic association
I don’t know what you have to know about a lizard, but I assume it’s different from a hamster.

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!

A text message exchange where author wrote “hey. hey. active storage uses a polymorphic table. there’s a polymorphic table in my computer RIGHT NOW”
A text message exchange where author wrote “hey. hey. active storage uses a polymorphic table. there’s a polymorphic table in my computer RIGHT NOW”
My honest-to-Buddha initial reaction sent to a developer friend upon finding this out. Just realizing now it says right there this happened on a Saturday morning. #bootcamplyfe

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.

Software Developer with social justice roots. I love cats, Star Trek, and singing to inspire the downtrodden.