In Rails, an association is a connection between two Active Record models. With associations we get convenient utility methods to easily query and do operations on those models.
Rails has the following associations belongs_to
, has_many
, has_one
, has_one :through
, has_many :through
, has_and_belongs_to_many
Active Record maintains primary key and foreign key information between instance of the two models that are related by one of the above associations. But our code doesn't explicitly have to worry about these keys in most cases.
Let's look at each association one by one and see how to declare them in the model class and what the migrations look like. Here are the links for a quick reference:
belongs_to
has_many
has_one
When to use has_one vs. belongs_to
has_one :through
has_many :through
has_and_belongs_to_many
Types of Associations
We'll start with the one-to-one associations, then cover one-to-many and many-to-many.
belongs_to
Let's start with an example:
class Car < ApplicationRecord
belongs_to :owner
end
With belongs_to
, each instance of the declaring model Car
is connected to or "belongs to" one instance of the other model Owner
.
This means that each Car
record will have a owner_id
column that will point to the id
column of an Owner
record.
At the migration level, it looks like this
create_table :cars do |t|
t.belongs_to :owner, foreign_key: true
# ...
end
Adding the foreign_key: true
is not required but doing so adds a database level foreign key constraint.
All this allows us to write @car.owner
in our code to get the Owner
object pointed to by a @car
.
So far, the relationship is one directional. The @car
knows about its owner, but the owners do not know about their cars.
We can change that with has_many
.
has_many
Continuing the above example, let's say the goal is to be able write @owner.cars
and get a list of cars that belong to a given owner. Here is how we can do that
class Owner < ApplicationRecord
has_many :cars
end
Now we can write @owner.cars
and it will work. We can also create a new car belonging to a given owner with @new_car = @owner.cars.create(...)
Note that in has_many :cars
, cars is plural. And also note that for belongs_to :owner
the model name needs to be singular. This is one-to-many relatiionship.
has_one
has_one
is similar to has_many
but is used create a bi-directional relationship on the "other side" of belongs_to
when we know that there is only one instance of the other model that has a reference to this model.
This creates a one-to-one relationship, while has_many
creates a one-to-many relationship.
class Supplier < ApplicationRecord
has_one :account
end
This implies that we want to model suppliers in our applications such that they have only one account. We can access this by @supplier.account
In this case the Account
would have a supplier_id
column that is a foreign key to id
column in Supplier
model.
The migration would look like this, it adds a unique index as well as and a foreign key constraint at the database level
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
If you noticed that the migration actually uses belongs_to
on the Account
while the association is defined as has_one
on the Supplier
class, yes that is a bit confusing.
So, let's clear up when to use has_one
vs. belongs_to
.
When to use has_one vs. belongs_to for a one-to-one relationship
Deciding which association to use comes from thinking about the meaning of the data in the real world. In the account vs. supplier case, it makes sense for 'a supplier to own an account' but it makes less sense to say 'an account owns a supplier' I guess.
Side note: there is an concept known as Domain Driven Design (and a book by the same name) that advocates for matching the data model - the names of tables and their relationships - as closely as possible to the real world names used in that domain. This makes it easier to query and work with the model in our product but also makes it easier to communicate about the design to all functions in a team. It give a common language between product managers and developers for example (e.g. when I worked for a healthtech startup, we used terms like 'claims', 'deductible', 'co-insurance' in our application). Anyway, data modeling is a science and an art.
Back to has_one
vs. belongs_to
. One thing we can keep in mind is that the foreign key is always on the belongs_to
side. So if an account belongs to a supplier, account has a foreign key supplier_id
pointing to a supplier.
The migration for the Account
table can be either:
create_table :accounts do |t|
t.bigint :supplier_id
# ...
end
or we can say t.references :supplier
or t.belongs_to :supplier
. They mean the same thing.
Let's look at one more flavor of one-to-one relationship with has_one :through
. And then we'll move on to many-to-many associations.
has_one :through
has_one :through
association connects the declaring model to one instance of another model by way of a third model. That's a bit abstract, let's see an example:
class Customer < ApplicationRecord
has_one :account
has_one :account_attribute, through: :account
end
class Account < ApplicationRecord
belongs_to :customer
has_one :account_attribute
end
class AccountAttribute < ApplicationRecord
belongs_to :account
end
All this is saying is that we have a Customer
model that is associated with one Account
. There are attributes for each account that we store in separate model called AccountAttribute
for denormalization. Now if the Customer
needs to refer to AccountAttribute
it needs to go through the associated Account
. Since account_attribute
table will have an account_id
foreign key and account
table will have a customer_id
foreign key. We can say @customer.account_attribute
The migration for those tables would look like this
class CreateCustomerAccountAttributes < ActiveRecord::Migration[7.0]
def change
create_table :customers do |t|
t.string :name
# ...
end
create_table :accounts do |t|
t.belongs_to :customer
# ...
end
create_table :account_attributes do |t|
t.belongs_to :account
# ...
end
end
end
So has_one :through
association is for when you have to traverse more than one model to get at the one-to-one relationship we want to express.
Next, let's move on to many-to-many associations.
has_many :through
A has_many :through
association sets up a many-to-many connection with another model through a third model.
For example, for a book tracking app that keeps track of which books a user has read over time, we may have a Book
and a Reader
model. And a many-to-many relationship called Readings
that tracks all the books read by a given reader and all the readers of a given book.
For this readings relationship, we may want to store additional fields on the relationship itself. For example, in addition to book_id
and reader_id
, we may want to store when the user read the book in a field called start_date
and end_date
.
The models would look like this
class Reader < ApplicationRecord
has_many :readings
has_many :books, through: :readings
end
class Readings < ApplicationRecord
belongs_to :reader
belongs_to :book
end
class Book < ApplicationRecord
has_many :readings
has_many :readers, through: :readings
end
And notice the additional field on Readings
in the migration
class CreateReadings < ActiveRecord::Migration[7.0]
def change
create_table :readers do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.string :name
t.timestamps
end
create_table :readings do |t|
t.belongs_to :reader
t.belongs_to :book
t.datetime :start_date
t.datetime :end_date
t.timestamps
end
end
end
has_and_belongs_to_many
has_and_belongs_to_many
creates a direct many-to-many connection with another model. There is no intermediate model to go through (though we do need to create a join table in the migration, see below).
Here is an example of the code and migration for Car
and Part
, where a car can have many parts and a part can be in many cars.
class Car < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :cars
end
The migration
class CreateCarsAndParts < ActiveRecord::Migration[7.0]
def change
create_table :cars do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :cars_parts, id: false do |t|
t.belongs_to :car
t.belongs_to :part
end
end
end
This association makes sense when we know that there are no fields we need to store on the relationship itself. This depends on the real world meaning of the relationship and domain of the models.
That is the main difference between has_and_belongs_to_many
and has_many :through
. In terms of which to choose in practice, I've found that most of the time has_many :through
makes more sense as it gives us an option to store fields on the relationship.
One last note, we are responsible for maintaining our database schema to match our associations. In practice, this means creating join table needed for has_and_belongs_to_many
associations.