What are polymorphic associations? When do we need to create join tables? When do we use single table inheritence?
We will cover some more advanced topics with Active Record Associations in this post.
Note: This post assumes that you are already familiar with common Active Record associations. You can see this post for Active Record associations basics for a refresher.
Data modeling is a skill that is an art and a science. In order to get it right, we need to think deeply about the natural relationship between our models in the real world. Active Record associations provide many options to model our business domain. If our models do not neatly fit into the common associations, we can use some of the concepts below and manually define our relationships.
What are Polymorphic Associations
If we need a model to belong to more than one other model on a single association, we'd use polymorphic associations.
Let's see this with an example. With the models below, we have
Product. Both of those can have many
Picture. So we make polymorphic association called
class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end class Employee < ApplicationRecord has_many :pictures, as: :imageable end class Product < ApplicationRecord has_many :pictures, as: :imageable end
The interesting part here is the
belongs_to :imageable, Polymorphic: true. This implies that we will have an
Picture that will a foreign key to either
Product. In order to know which one we will also have an
The migration for
Picture looks like this
class CreatePictures < ActiveRecord::Migration[7.0] def change create_table :pictures do |t| t.string :name t.bigint :imageable_id t.string :imageable_type t.timestamps end add_index :pictures, [:imageable_type, :imageable_id] end end
Another way to create the
imageable_type is with
t.references :imageable, polymorphic: true. I like the first one as it's more explicit and makes it clear that we will have two fields id and type.
Creating Join Tables
has_and_belongs_to_many association, we need to create a join table in our migration. This join table should not have a primary key. This is important for things to work correctly. This join table does not represent a model. (Note: if we want a join table that can hold other fields, we use
has_many :through association).
There are two ways to create a table without primary key
id. We can use
create_join_table in the migration or set
false if using
create_table like this
ActiveRecord::Migration[7.0] def change create_table :authors_books, id: false do |t| t.bigint :author_id t.bigint :book_id end
Bi-directional Associations and inverse_of
Active Record will automatically identify most bi-directional associations with standard names. By bi-directional association we mean having
belong_to on one model and
has_many on the "opposite" model, for example.
In case we decide to use non standard names though, we need to manually specify the "opposite" model. We can do this with
With the below model, we are using
writer instead of
author so now
@book.writer works to refer to the
Author object, but
@author doesn't 'know' about the relationship if we leave out the
inserve_of on the
class Author < ApplicationRecord has_many :books, inverse_of: 'writer' end class Book < ApplicationRecord belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end
There are four callbacks that can specify methods to be called when an object is added for removed from a collection through the association.
We specify the callback as an option to the association declaration.
class LibraryPatron < ApplicationRecord has_many :books, before_add: :checkout_limit def checkout_limit(book) throw(:abort) if limit_reached? end end
With that in place, a library patron will only be allowed to add books if their checkout limit is not reached.
These are similar to
before_save callback that causes something to happen before an object is saved. The association callbacks only happen when an object is added or removed through the association collection.
Single Table Inheritance
Sometimes we want to share fields and behavior between different models but also have specific behavior for each and separate controllers too. With Single Table Inheritance (STI), we do this by storing the related models in a single table but have a
type column in that table that differentiates them.
For a concrete example, let's say we want to represent different types of vehicles that all have
price fields in common. We can create a
$ rails generate model vehicle type:string color:string price:decimal
Then we can generate a
car which is a type of
vehicle like this
$ generate model car --parent=Vehicle
That will generate a model that look like this
class Car < Vehicle end
And when we create a
Car object, it will be stored in the
vehicles table with the
type column set to "Car"
Car.create(color: 'red' price: 10000)
with this SQL
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
The above explanation and examples give us a better idea of all the ways in which we can define Active Record associations in our applications.