Rails Glossary > Associations

Associations

Rails associations establish connections between Active Record models. With them, we can define how our models interact and save time an duplication while allowing better model interactions.

In Rails, associations are a powerful way to define relationships between models in an intuitive and expressive manner.

Establishing these connections, makes working with the framework easier by reducing duplication or tedious operations with data that's naturally associated.

Let's explore what can associations do for us and why do they exist:

Why associations

The main reason associations exist in Rails is that, without them, we would have to write a lot of code to handle data that's associated with our records.

We would also have to worry about data integrity issues like orphaned records, that arise because of deleting a given record and forgetting or failing to delete associated data.

In Rails associations are declared using macros, which are methods that can generate or modify methods at runtime:

class User < ApplicationRecord
  has_many :articles
end

This macro adds many methods to the user model that are useful to query and manipulate associated records.

These methods know how to query the database to return the associated Article collection that belongs to the user and avoid us the need to query using Active Record:

user = User.first
articles = user.articles
articles = Article.where(user_id: user.id) ## Alternative to using associations

Moreover, we can pass the dependent: :destroy argument to the association:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

Which will destroy associated articles if we delete a given user. This saves us from having to write repetitive code every time we need to destroy records:

user = User.first
articles = Article.where(user_id: user.id)
articles.each do |article|
  article.destroy
end
user.destroy

Types of associations in Rails

Rails provides six types of basic associations and some advanced associations to help us model our data layer to fit our needs.

Note that these associations are not exclusively related to Rails, but they are a convenient time saver and can also avoid

Let's start by understanding the belongs_to association:

Belongs to

Belongs to association diagram

The belongs-to association establishes a relationship with another model, given that the model that establishes the belongs_to relates to one instance of the associated model.

class Article < ApplicationRecord
  belongs_to :user
end

In this case, we know that an article belongs to one user, which is the author.

By convention, and unless we define it otherwise, this association expects a user_id foreign_key in the articles table.

It also makes the Article instances “know” about the user that it belongs to, but it doesn't create the inverse relation unless we declare it in the User class.

This means that, to allow users to be associated with multiple articles, we would need to declare a has_many or has_one association in the User class depending on how we want to model our data.

Has one

Has one association diagram

The has_one association is useful when we intend to declare that two models can only be related with one instance of each other.

For example, we might want to store the user's profile information in a separate profiles table with a corresponding Profile model.

However, a given user normally has only one profile, so we can use the has_one in this case.

class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user
end

Setting both associations would give the User access to the .profile method, which would return a single Profile instance that corresponds to the first profile associated with the user.

Note that declaring the association doesn't validate that a user can only create one profile, so we might want to add a validation to make sure that we cannot add multiple profiles for a user:

class Profile < ApplicationRecord
  belongs_to :user
  validates :user_id, uniqueness: true
end

This is a good start but enforcing uniqueness at the application level isn't enough to guarantee that no duplicate profile can be created at the database level.

To solve this, we can add an index with a migration:

class AddUniqueIndexToProfiles < ActiveRecord::Migration[8.0]
  def change 
    add_index :profiles, :user_id, unique: true
  end
end

Has Many

Has many association diagram

The has_many association creates a one-to-many relationship with other models, meaning that a model can have zero or multiple instances of another model.

It's declared like this:

class Author < ApplicationRecord
  has_many :books
end

It's implied that the Book model's table will have a foreign key that references the author. By default, this key would match the model in singular: article_id but we can customize that if it suits us.

Like stated before, many book related methods are defined by the has_many :books macro. These methods allow us to build, create, query and manipulate the related records.

Just like before, if we would like to add referential integrity at the database level, we would have to add the foreign_key: true when creating the books table:

create_table :books do |t|
  t.references :author, foreign_key: true
end

This will make sure that no book can be created without a valid author: if we try to associate a book with an author_id that doesn't exist, we would meet with an error at the db level.

Has many through

Has many through association diagram

The has_many :through association sets up a many-to-many relationship using a join model, which relates two models via an intermediary.

For this example, we will model a user that can place orders within our application. At some point, we might want to retrieve every purchased item for a given user.

The has_many :through association is helpful for a case like this. The association setup looks like this:

class User < ApplicationRecord
  has_many :orders
end

Then, we set the associations in the Order model:

class Order < ApplicationRecord
  belongs_to :user
  has_many :order_items
end

Conversely, the OrderItem model would declare a belongs_to:

class OrderItem < ApplicationRecord
  belongs_to :order
end

Finally, we associate the users with the order items:

class User < ApplicationRecord
  has_many :orders
  has_many :order_items, through: :orders
end

If we find that the join model—the order item in this case—doesn't do anything beyond connecting the User and Order models, we can use a has_and_belongs_to_many association, even though, in the long term, that's rarely the case, and we are usually better of using this type of association.

Has one through

Has one through association diagram

The has_one :through association establishes a one-to-one relationship with another model through an intermediary model.

It's very similar to the has_many :through except we get access to one instance of other models through a third one.

In this example, each order item represents a product that has been purchased. And, because each product only has one brand, we can access the brand for it from the OrderItem model.

To achieve this, we would have to set the associations like this:

class OrderItem < ApplicationRecord
  belongs_to :product
  has_one :brand, through: :product
end

Then, we would have to set the association between the Product and the Brand:

class Product < ApplicationRecord
  belongs_to :brand
  has_many :order_items
end

We would then be able to access the brand of order items using order_item.brand which

Has and belongs to many

Has and belongs to many association diagram

The has_and_belongs_to_many association is very similar to the has_many :through with one difference: it requires a join table but not a corresponding model and the join table is not required to have a primary key.

In the example shown above, we're modeling a learning platform where users can enroll in courses.

To define the has_and_belongs_to_many associations, we don't need to define a Enrollment model, just to have a join table that follows a convention: it should be the result of joining both model names in plural and alphabetical order.

Hence, we get the course_users table.

Now, to declare the association, we need to add the has_and_belongs_to_many macro to both models:

class User < ApplicationRecord
  has_and_belongs_to_many :courses
end
class Course < ApplicationRecord
  has_and_belongs_to_many :users
end

You might feel that the course_users table is not very expressive, and you would probably be right, so we can rename the table to something like enrollments:

![[has-and-belongs-to-many-custom-table-name.png]]

Now, we just need to specify the table name to the association using the join_table argument:

class User < ApplicationRecord
  has_and_belongs_to_many :courses, join_table: "enrollments"
end
class Course < ApplicationRecord
  has_and_belongs_to_many :users, join_table: "enrollments"
end

Now, everything should work the same as before, but now we have a more meaningful join table.

As a note, if we want to create a join table without a primary key, we would have to use the create_join_table method in the migration.

Consider that prescinding of a PK for this join table can give us minor performance gains and make the table more simple, but we have to put some thought before deciding to avoid regretting it in the future.

The has_and_belongs_to_many association is a practical solution when we're sure that our join table won't need extra attributes or behavior in the long term. However, it's not uncommon to add them with time. If we're not sure whether the table will need extra attributes or behavior eventually, we are better suited using a has_many :through association.

Customizing Associations

Rails associations can be customized if your database tables or model names do not correspond directly with the defaults.

This means that we can declare associations with models whose names don't exactly match the name of the association or that have table names that differ from the model's name.

For example, we could have a User that acts as an author for articles without having to create a separate Active Record model:

class Article < ApplicationRecord
  belongs_to :author, class_name: "User", foreign_key: :user_id
end

Using the class_name we can tell the Article which class we expect to relate to, and the foreign_key lets the association know that we're expecting a user_id on the articles table.

We can also customize the foreign_key to match our association name by having an author_id column that acts as a foreign key in the articles table and making that explicit in the association:

class Article < ApplicationRecord
  belongs_to :author, class_name: "User", foreign_key: :author_id
end

Of course, these changes only make sense if the semantical gains are worth more to our codebase than deviating from Rails conventions.

Because of the presence of the foreign_key in this customization, Active Record will not recognize the bidirectional association between the author and the article, which will lose us access to some association goodies.

That's why we can make the relation explicit using the inverse_of to customize the association without losing any features:

class Article < ApplicationRecord
  belongs_to :author, class_name: "User", foreign_key: :author_id, inverse_of: :article
end

Advanced associations

Rails provides us with three advanced associations besides the six basic ones we already covered.

These allow us to relate models in ways that are less intuitive but help us avoid duplication and let us better work with data when some specific scenarios arise:

Polymorphic associations

Polymorphic association diagram

A polymorphic association, which gets its name from the OOP programming principle, establishes a relation between models that don't necessarily know about each other via their foreign keys.

It allows us to relate one model to multiple other models without explicitly defining the relation.

In the example above, a Bookmark is a model that can belong to posts or products or, really, an indefinite number of things.

The Bookmark model constructs the relation via the bookmarkable association, which is an arbitrary name we give to things that can be bookmarked.

This means that, for example, a user can bookmark posts or products using a single bookmarks table without the need to add hypothetical post_bookmarks and product_bookmarks tables and associating them with users.

They are very useful when we are confident that we will need our model to relate to other models in a generic way. Some good use cases for this type of association are:

  • Comments
  • Bookmarks
  • Reactions
  • Images
  • Votes
  • Tags
  • Reviews

To declare this association in the example shown above, we first need to start with the polymorphic model:

class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :bookmarkable, polymorphic: true
end

Then, we declare the inverse association for posts and products:

class Post < ApplicationRecord
  has_many :bookmarks, as: :bookmarkable
end

class Product < ApplicationRecord
  has_many :bookmarks, as: :bookmarkable
end

Here, the bookmarkable object would be the one declaring the has_many so, calling @bookmark.bookmarkable would return a Post instance when it's a post and a Product otherwise.

And, products and posts would have access to the bookmarks method which would return the bookmarks associated with them, which also contain the user for every instance.

As our application grows, we can add the has_many :bookmarks, as: :bookmarkable to as many models as we require and things should work without many issues.

When adding a polymorphic association like this, it's a good idea to add an index to tie the type and id attributes of the polymorphic table:

class CreateBookmarks < ActiveRecord::Migration[8.0]
  def change
    create_table :bookmarks do |t|
      t.bigint :bookmarkable_id
      t.string :bookmarkable_type
      t.timestamps
    end

    add_index :bookmarks, [:bookmarkable_id, :bookmarkable_type]
  end
end

One last bit of advice: if you decide to change the class names of the bookmarked items, say you want to change from Post to Article you have to update the corresponding bookmarks table to keep everything working as expected.

Composite primary keys

A composite primary is a primary key that's composed of multiple columns.

It means that, instead of being able to find a record with a single key like Product.find(4) we would need to pass various attributes to retrieve a given product.

We can use these composite keys when we want to keep record integrity to a new level:

product = Product.find([1, "RCMM-1025"]) # => Find by id and SKU

If we wish to set a composite key when creating a migration, we pass the desired attributes to the primary_key argument:

class CreateEnrollments < ActiveRecord::Migration[8.0]
  def change
    create_table :enrollments, primary_key: [:course_id, :user_id] do |t|
      t.integer :course_id
      t.integer :user_id
    end
  end
end

Now, we need to specify our model to use a composite PK:

class Enrollment < ApplicationRecord
  self.primary_key = [:course_id, :user_id]

  belongs_to :course
  belongs_to :user
end

However, before deciding whether using a composite PK is adequate for your use case, consider that they can result in performance degradation and increased application logic.

Self Joins

The self-join association is one where the foreign_key is located within the same table of the model that declares it.

They are useful to model relationships between different instances of a same class, especially when we need some sort of hierarchy

For example, if we wanted to model a category hierarchy where items belong to categories or subcategories, we could achieve it using a single Category table with self-joins:

class Category < ApplicationRecord
  belongs_to :parent, class_name: "Category", optional: true
  has_many :subcategories, class_name: "Category", foreign_key: "parent_id"
end

The migration would look something like this:

class CreateCategories < ActiveRecord::Migration[8.0]
  def change
    create_table :categories do |t|
      t.string :name, null: false
      t.references :parent, foreign_key: { to_table: :categories }, index: true, null: true

      t.timestamps
    end

    add_index :categories, [:name, :parent_id], unique: true
  end
end

By declaring an optional parent_id field, we could consider categories with no parent_id as root categories, and we could model a tree structure where categories could have children, grandchildren, fathers, grandfathers, and so on.

class Category < ApplicationRecord
  belongs_to :parent, class_name: "Category", optional: true
  has_many :subcategories, class_name: "Category", foreign_key: "parent_id"

  scope :root_categories, -> { where(parent_id: nil) }

  def root?
    parent_id.nil?
  end

  def ancestors
    return [] if root?

    ## Recursive traversal of ancestors returning
    ## an array with the ancestor list
    parent.ancestors + [parent]
  end

  def descendants
    # Recursive traversal of descendants that returns a
    # flattened array with the Category instances
    subcategories.map { |subcat| [subcat] + subcat.descendants }.flatten
  end
end

Now, items can declare a belongs_to :category_id and be related to a category without knowing specifically about the hierarchy.

Other associations

Rails offers access to other associations like Single Table Inheritance and Delegated Types.

Even though these associations are very useful for some use cases, they escape the scope of this article. We will talk about them in future articles so you can learn more about them and how to use them in your applications.

Meanwhile, you can learn about delegated types in the article we wrote about building the kanban board feature for Avo.

Stay in touch to learn more about advanced associations in Rails.

Summary

Rails associations transform how you work with related data, making your code more expressive and maintainable. By understanding the different types of associations and their options, you can model complex relationships with ease. Whether you're building a simple blog or a complex application with intricate data relationships, mastering associations is essential for effective Rails development.

The true power of associations lies not just in the syntax they provide, but in how they encourage you to think about your data in terms of relationships. This mental model aligns closely with how we naturally think about information, making your code not only more efficient, but also more intuitive to write and understand.

Try Avo for free