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:
belongs_to
has_one
has_many
has_many :through
has_one :through
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
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
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
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
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.