Back to home

Understanding Rails Associations main image

Understanding Rails Associations

Associations in Rails help us to map together the relationship between different ActiveRecord models.

These make our operations simpler and can help us marry together the different relationships between entities in an understandable manner.

This post will focus on understanding the different associations that are supported by Rails and demonstrate how each of them work.

Source code can be found here.

Prerequisites

  1. Basic familiarity with setting up a new Rails project.

Getting started

We will use Rails to initialize the project demo-rails-associations:

# Create a new rails project $ rails new demo-rails-associations $ cd demo-rails-associations

At this stage, our project is now ready to start working with.

The six associations

In Rails, there are six support associations:

  1. belongs_to
  2. has_one
  3. has_many
  4. has_many :through
  5. has_one :through
  6. has_and_belongs_to_many

In the case of today, we will be focusing on the (1), (2), (3) and (6).

Each of the above help us to map out one-to-one, one-to-many and many-to-many relationships between entities.

Declaring these associations between two models can provide utility functionality as well as a requirement to maintain primary key and foreign key information between different instances of the model.

For the remainder of this post, we will walk through an example of each of the six.

One-to-one associations

The keyword has_one is associated with a singular model while belongs_to can be associated with both one-to-one and one-to-many. The later is applied to the model that is referenced via a foreign key.

A contrived example of a one-to-one model would be to state that one book is written by one author.

If we model a one-to-one relationship this way means that there can only ever be one author associated with one book and not many. This may not be the ideal example: one book can have one or more authors. We will use our understanding of this to move onto the one-to-many relationship. Understanding that, let's move on.

# Generate a basic Author model $ bin/rails g model Author name:string # Generate a basic Book model $ bin/rails g model Book title:string # Run the pending migrations generated from the above (you may need to create the db first) $ bin/rails db:migrate

As things currently stand, we have two separate models created with no relationship between them:

Current model standing

Current model standing

In order to actually associate the models, we can create a migration that will link the two:

# Write out a new migration $ bin/rails g migration AddBookToAuthor book:references

In the created migration file, you can update it to add a reference:

class AddBookToAuthor < ActiveRecord::Migration[7.0] def change add_reference :author, :book, null: false, foreign_key: true end end

At this point, our entity-relation diagram now looks like this:

One-to-one relationship

One-to-one relationship

We can now play around with the console to see this in action:

# Creating an Author irb(main):001:0> @author = Author.create(name: "Bob") => #<Author:0x00007fc5a60bef78 id: 1, name: "Bob", created_at: Wed, 16 Mar 2022 04:05:46.384787000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:05:46.384787000 UTC +00:00> # Creating a one-to-one relationship with a book irb(main):002:0> @author.book = Book.create(title: "First book") => #<Book:0x00007fc5a52a7ef8 id: 1, title: "First book", created_at: Wed, 16 Mar 2022 04:06:03.739917000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:06:03.739917000 UTC +00:00, author_id: 1> # List all the books irb(main):003:0> Book.all Book Load (0.1ms) SELECT "books".* FROM "books" => [#<Book:0x00007fc59f684770 id: 1, title: "First book", created_at: Wed, 16 Mar 2022 04:06:03.739917000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:06:03.739917000 UTC +00:00, author_id: 1>]

Updating to one-to-many

At the moment, we have our first issue: what happens when an author has more than one book?

To resolve this problem, let's move to the next relationship: one-to-many.

Remove the AddBookToAuthor migration file, reset our database and create a new migration file AddBooksToAuthor.

# Add new migration file $ bin/rails g migration AddBooksToAuthor book:references # Reset our database $ bin/rails db:migrate:reset

The new migration file looks very similar to before:

class AddBooksToAuthor < ActiveRecord::Migration[7.0] def change add_reference :authors, :book, null: false, foreign_key: true end end

What we need to do is update our author model from has_one to has_many:

class Author < ApplicationRecord has_many :books end

At this point, our relationship now looks like this:

One-to-many author-to-books relationship

One-to-many author-to-books relationship

Now we can reload the Rails sandbox console and demonstrate this in action:

irb(main):001:0> @author = Author.create(name: "Bill") TRANSACTION (0.0ms) SAVEPOINT active_record_1 Author Create (9.4ms) INSERT INTO "authors" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Bill"], ["created_at", "2022-03-16 04:32:17.011341"], ["updated_at", "2022-03-16 04:32:17.011341"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 => #<Author:0x00007f9b2c51fee0 ... irb(main):002:0> @author.books << Book.create(title: "Book one") # Returns books irb(main):003:0> @author.books << Book.create(title: "Book two") # Returns books irb(main):004:0> @author.books => [#<Book:0x00007fd004d36068 id: 1, title: "Book one", created_at: Wed, 16 Mar 2022 04:38:13.248028000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:38:13.248028000 UTC +00:00, author_id: 1>, #<Book:0x00007fd004f4a548 id: 2, title: "Book two", created_at: Wed, 16 Mar 2022 04:38:17.527201000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:38:17.527201000 UTC +00:00, author_id: 1>] irb(main):005:0> Book.all => [#<Book:0x00007fd004d36068 id: 1, title: "Book one", created_at: Wed, 16 Mar 2022 04:38:13.248028000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:38:13.248028000 UTC +00:00, author_id: 1>, #<Book:0x00007fd004f4a548 id: 2, title: "Book two", created_at: Wed, 16 Mar 2022 04:38:17.527201000 UTC +00:00, updated_at: Wed, 16 Mar 2022 04:38:17.527201000 UTC +00:00, author_id: 1>]

At this stage, we have successfully implemented a one-to-many relationship between the author and the books.

The next problem naturally follows from the first problem that we ran into: authors can have more than one book, but books can also have more than one author.

In order to more accurately create this relationship, we will move onto many-to-many.

Creating our many-to-many relationship

Again, let's delete our AddBooksToAuthor migration and do one more for our CreateBooksAndAuthors migration:

# Add new migration file $ bin/rails g migration CreateAuthorsBooks author:references book:references

For the created migration file, it has the following:

class CreateAuthorsBooks < ActiveRecord::Migration[7.0] def change create_table :authors_books do |t| t.references :author, null: false, foreign_key: true t.references :book, null: false, foreign_key: true t.timestamps end end end

Now we can reset and migrate that code.

# Reset our database $ bin/rails db:migrate:reset

Next, we have to update the app/models/author.rb file:

class Author < ApplicationRecord has_and_belongs_to_many :books end

Then, we also have to update the app/models/books.rb file:

class Book < ApplicationRecord has_and_belongs_to_many :authors end

At this point, our entity-relationship diagram now looks like this:

Many-to-many

Many-to-many

To test this out again on the console, fire it up with bin/rails c (or reload! in the console) and run the following:

# Create first author irb(main):001:0> @author = Author.create(name: "Joe") (0.1ms) SELECT sqlite_version(*) TRANSACTION (0.1ms) begin transaction TRANSACTION (0.1ms) SAVEPOINT active_record_1 Author Create (0.4ms) INSERT INTO "authors" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Joe"], ["created_at", "2022-03-16 05:18:13.705148"], ["updated_at", "2022-03-16 05:18:13.705148"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 => #<Author:0x00007f883f91dc90 ... # Add a couple of books to our author "Joe" irb(main):002:0> @author.books << Book.create(title: "Book one") TRANSACTION (0.0ms) SAVEPOINT active_record_1 Book Create (0.7ms) INSERT INTO "books" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Book one"], ["created_at", "2022-03-16 05:19:15.999610"], ["updated_at", "2022-03-16 05:19:15.999610"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 TRANSACTION (0.1ms) SAVEPOINT active_record_1 Author::HABTM_Books Create (0.8ms) INSERT INTO "authors_books" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["author_id", 1], ["book_id", 1], ["created_at", "2022-03-16 05:19:16.014072"], ["updated_at", "2022-03-16 05:19:16.014072"]] TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 Book Load (0.1ms) SELECT "books".* FROM "books" INNER JOIN "authors_books" ON "books"."id" = "authors_books"."book_id" WHERE "authors_books"."author_id" = ? [["author_id", 1]] => [#<Book:0x00007f883a665de0 id: 1, title: "Book one", created_at: Wed, 16 Mar 2022 05:19:15.999610000 UTC +00:00, updated_at: Wed, 16 Mar 2022 05:19:15.999610000 UTC +00:00>] irb(main):003:0> @author.books << Book.create(title: "Book two") TRANSACTION (0.1ms) SAVEPOINT active_record_1 Book Create (0.1ms) INSERT INTO "books" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Book two"], ["created_at", "2022-03-16 05:19:34.239823"], ["updated_at", "2022-03-16 05:19:34.239823"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 TRANSACTION (0.1ms) SAVEPOINT active_record_1 Author::HABTM_Books Create (0.2ms) INSERT INTO "authors_books" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["author_id", 1], ["book_id", 2], ["created_at", "2022-03-16 05:19:34.242367"], ["updated_at", "2022-03-16 05:19:34.242367"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 => [#<Book:0x00007f883a665de0 id: 1, title: "Book one", created_at: Wed, 16 Mar 2022 05:19:15.999610000 UTC +00:00, updated_at: Wed, 16 Mar 2022 05:19:15.999610000 UTC +00:00>, #<Book:0x00007f88354f0938 id: 2, title: "Book two", created_at: Wed, 16 Mar 2022 05:19:34.239823000 UTC +00:00, updated_at: Wed, 16 Mar 2022 05:19:34.239823000 UTC +00:00>] irb(main):004:0> @book_one = Book.first Book Load (0.1ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? [["LIMIT", 1]] => #<Book:0x00007f883f0201d8 ... # Add another author to the first book irb(main):008:0> @book_one.authors << Author.create(name: "Jill") TRANSACTION (0.1ms) SAVEPOINT active_record_1 Author Create (0.1ms) INSERT INTO "authors" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Jill"], ["created_at", "2022-03-16 05:20:35.609148"], ["updated_at", "2022-03-16 05:20:35.609148"]] TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 TRANSACTION (0.1ms) SAVEPOINT active_record_1 Book::HABTM_Authors Create (0.1ms) INSERT INTO "authors_books" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["author_id", 2], ["book_id", 1], ["created_at", "2022-03-16 05:20:35.619110"], ["updated_at", "2022-03-16 05:20:35.619110"]] TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 Author Load (0.1ms) SELECT "authors".* FROM "authors" INNER JOIN "authors_books" ON "authors"."id" = "authors_books"."author_id" WHERE "authors_books"."book_id" = ? [["book_id", 1]] => [#<Author:0x00007f88356b4bc0 id: 1, name: "Joe", created_at: Wed, 16 Mar 2022 05:18:13.705148000 UTC +00:00, updated_at: Wed, 16 Mar 2022 05:18:13.705148000 UTC +00:00>, #<Author:0x00007f883e8a59d0 id: 2, name: "Jill", created_at: Wed, 16 Mar 2022 05:20:35.609148000 UTC +00:00, updated_at: Wed, 16 Mar 2022 05:20:35.609148000 UTC +00:00>]

At this point, we have our many-to-many relationship up and working.

Summary

Today's post demonstrated how to create a one-to-one, one-to-many and many-to-many relationship in Rails.

In the next post, we will be looking at how this can be done with the :through keywords, and then finally a follow up post will demonstrate how we can visualize these relationships with the rails-erd gem.

Resources and further reading

Photo credit: sarringt

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.