Ruby on Rails Models Association Basics - Let's Build a Bookstore App - First Part
In Ruby on Rails Dec 22, 2023
Updated on Jan 4, 2024
Welcome to my tutorial focusing on Active Record associations in Ruby on Rails. Rails offers a powerful toolkit comprising six primary types of associations: belongs_to, has_one, has_many, has_many :through, has_one :through, and has_and_belongs_to_many. In this guide, we'll delve into these associations while constructing a bookstore application using Ruby on Rails 7.1.2 and Ruby 3.2.2.
tutorial parts:
- Ruby on Rails Models Association Basics - Let's Build a Bookstore App - First Part - Set Up the Application
- Ruby on Rails Models Association Basics - Let's Build a Bookstore App - Second Part
SET UP THE APPLICATION
Let's start by creating a new application. In the terminal, we type:
rails new bookstore
Let's change directories into this folder:
cd bookstore
To generate data for the database, we will use the Faker gem. Faker generates
fake data, which can be useful for populating a database with placeholder information for testing and development
purposes. Add this gem to the Gemfile
in the development and test group:
gem 'faker'
Then run:
bin/bundle install
Generate and Edit the Book Resource
We will generate a scaffold for the Book
resource. Let's type in the terminal:
bin/rails g scaffold Books title:string
Let's run this migration:
bin/rails db:migrate
Let's add some validations in the book.rb
file in the app/models/
folder:
validates_presence_of :title
validates_uniqueness_of :title
Let's add a method to the db/seeds.rb
file to create some book records in the database:
20.times do
book = Book.create(
title: Faker::Book.title,
)
end
Now let's run:
bin/rails db:seed
Let's set the root
route to the books
index
action. In
config/routes.rb
at this line at the top:
root "books#index"
Let's edit the _book.html.erb
partial in the app/views/books/
folder. Replace the content
with this:
<div id="<%= dom_id book %>">
<%=link_to book.title, book %>
</div>
And we will edit the index
view as well:
<h1>Books</h1>
<p><%= link_to "New Book", new_book_path %></p>
<ul id="books">
<% @books.each do |book| %>
<li style="margin-bottom: 1rem"><%= render book %></li>
<% end %>
</ul>
On the home page of our application, we have a list of links to our books' details.
Building the Author Resource
Books have authors, so let's continue developing our application by generating an Author
model:
bin/rails g model Author name:string
We run the migration:
bin/rails db:migrate
Let's add validations for this model in the app/models/author.rb
file:
validates_presence_of :name
validates_uniqueness_of :name
Now, let's add some authors to our database. In the db/seeds.rb
file, above the method for generating
book records, add this:
authors = 10.times.map { Author.create(name: Faker::Book.author) }
Now, we can run in the terminal:
bin/rails db:reset
This command refreshes the database to match the recent changes we've made. It creates new book and author records. Avoid using this in production as it erases all existing data.
Let's generate an Authors
controller:
bin/rails g controller Authors
To this controller, we'll add two methods: index
and show
:
def index
@authors = Author.all
end
def show
@author = Author.find(params[:id])
end
In config/routes.rb
, we'll add routes for the Authors
resource:
resources :authors
And we'll create the index
and show
views in the app/views/authors/
folder.
In the index.html.erb
file, let's add a list of authors:
<h1>Authors</h1>
<ul>
<% @authors.each do |author| %>
<li style="margin-top: 1rem"><%= link_to author.name, author %></li>
<% end %>
</ul>
Then, in the show.html.erb
file, we add this:
<h1><%= @author.name %></h1>
<p><%= link_to "Back to authors", authors_path %></p>
We can now visit http://localhost:3000/authors and see the authors in our database.
Associate Authors with their Biographies
Authors have biographies, so let's add a Biography
model to the database. In the terminal, type:
bin/rails g model Biography content:text
Then run the migration:
bin/rails db:migrate
It's now time to create our first association. In the biography.rb
file in the app/models/
folder, we'll declare a belongs_to
association like this:
belongs_to :author
This association establishes an association between the Biography
and Author
models,
indicating that a biography record is associated with a specific author.
Given that an author typically has one associated biography, we will establish a bi-directional association between
the Author
and Biography
models using the has_one :biography
declaration in the
author.rb
file within the app/models/
folder. This association enables an author to be
linked to at most one biography, facilitating easy access to an author's specific biography within the application.
We'll add like this:
has_one :biography
The next step we will take is to create a foreign key constraint in the database. To do this, we'll generate a migration:
bin/rails g migration AddAuthorRefToBiography author:references
This migration will create an author_id
column in the biographies
table. Let's run this
migration:
bin/rails db:migrate
In the seeds.rb
file, let's add a method to create the biographies in the database for each of the
authors. Below the method that generates author records, add this:
authors.each do |author|
Biography.create(
content: Faker::Lorem.paragraphs(number: rand(3..6)).join("\n\n"),
author: author
)
end
We will reset the database to reflect the changes we made:
bin/rails db:reset
We are now able to access the author's biography. In the Authors
show
view, let's add this
after the author name:
<h2>Biography</h2>
<%= @author.biography.content %>
We can now visit the author's pages and see their biographies.
Building the PUBLISHER Resource
Every book has a publisher. So, we will need a Publisher
model in our database. Let's generate this
model:
bin/rails g model Publisher name:string
Next, let's run the migration:
bin/rails db:migrate
Let's add validations in the publisher.rb
file within the app/models/
folder:
validates_presence_of :name
validates_uniqueness_of :name
Let's add a method in our seeds.rb
file to generate some publisher records in the database:
6.times do
Publisher.create(
name: Faker::Book.publisher,
)
end
Now, we will run:
bin/rails db:reset
Next, we will generate a Publishers
controller:
bin/rails g controller Publishers
To this controller, we'll add two methods:
def index
@publishers = Publisher.all
end
def show
@publisher = Publisher.find(params[:id])
end
In config/routes.rb
, we'll add routes for the Publishers
resource:
resources :publishers
Let's now create the index
and the show
views.
Let's open the index.html.erb
file, and add this:
<h1>Publishers</h1>
<ul>
<% @publishers.each do |publisher| %>
<li style="margin-bottom: 1rem"><%= link_to publisher.name, publisher %></li>
<% end %>
</ul>
We show a list of publishers.
And in the show.html.erb
file, we'll add the publisher's name for now:
<h1><%= @publisher.name %></h1>
<p><%= link_to "Back to publishers", publishers_path %></p>
Create an Association Between The Book and the Publisher Model
It's the time to create an association between the Book
and the Publisher
model. The book
belongs to a publisher. So, we'll use the belongs_to
association. Let's edit the book.rb
file to include this association:
belongs_to :publisher
This association signifies that each book record is associated with a single publisher, allowing us to retrieve a book's publisher or link a book to its corresponding publisher.
Also, we need to set the association in the other direction. The publisher has many books. That's why here we will use
the has_many
association. Let's edit the publisher.rb
file:
has_many :books
As we did before, we must generate a migration to add a publisher_id
column in the books table:
bin/rails g migration AddPublisherRefToBooks publisher:references
There are already books in the database without publishers. Since it's a development environment, we can drop the database:
bin/rails db:drop
bin/rails db:create
bin/rails db:migrate
Now, let's edit the seeds.rb
file, to add a publisher to every book:
20.times do
book = Book.create(
title: Faker::Book.title,
publisher: Publisher.order("RANDOM()").first
)
end
Let's run:
bin/rails db:seed
With these changes, we can easily access the book's publisher. Let's edit the
app/views/books/show.html.erb
file. After the book partial add this:
<p><strong>Publisher: </strong> <%= link_to @book.publisher.name, @book.publisher %></p>
And also, we can access the publisher's books in the views. Let's edit the Publishers
show
view and add a list of publisher's books. Let's add the list after the publisher's name:
<h2>Books published by <%= @publisher.name %></h2>
<ul>
<% @publisher.books.each do |book| %>
<li><%= link_to book.title, book %></li>
<% end %>
</ul>
Create Associations Between the Book and the Author Models
Books can have more than one author. We need to reflect that in our database schema. That's why we can't use the
belongs_to
and has_many
associations between these two models. We will use the
has_many :through
association instead.
This association works through a third model, as the name says. Let's see how this works for our models. Let's first create a migration to generate the join model:
bin/rails g model BookAuthor book:references author:references
This command also adds the belongs_to :book
and belongs_to :author
to this model. And it creates a join table
(book_authors
) to link book and authors.
Let's run this migration:
bin/rails db:migrate
And we will set the corresponding associations for the Book
and Author
models. In the
book.rb
file, add these lines:
has_many :book_authors
has_many :authors, through: :book_authors
And in the author.rb
file, add these lines:
has_many :book_authors
has_many :books, through: :book_authors
Let's add a validation to the Book
model, to ensure there is at least one author associated with the
book:
validates :authors, presence: true
In the seeds.rb
file, let's edit the method for generating books to add authors to these books:
20.times do
book = Book.new(
title: Faker::Book.title,
publisher: Publisher.order("RANDOM()").first
)
authors_for_book = authors.sample(rand(1..3))
authors_for_book.each { |author| book.authors << author }
book.save if book.authors.present?
end
Let's reset the database:
bin/rails db:reset
Now we can easily access @book.authors
. Let's edit the Books
show
view to show
the book's authors. Before the publisher name add this:
<p>
<span>by</span>
<% @book.book_authors.each_with_index do |author, index| %>
<% if index > 0 %>, <% end %>
<%= link_to author.author.name, author.author %>
<% end %>
</p>
Also, we can easily access directly the @author.books
. Let's edit the Authors
show
view to show the list of author's books. After the author biography, add this:
<h2>Books</h2>
<ul>
<% @author.books.each do |book| %>
<li><%= render partial: 'books/book', locals: {book: book} %></li>
<% end %>
</ul>
Building a Genre Resource
A book must have at least one genre. Some books belong to more than one genre. So, a Genre
model must
exist in our database. Let's generate one:
bin/rails g model Genre name:string
Let's run the migration:
bin/rails db:migrate
Let's add some validations in the genre.rb
file:
validates_presence_of :name
validates_uniqueness_of :name
Let's also write a method to add some genres to the database. In db/seeds.rb
, add these lines before the
method generates book records:
8.times do
Genre.create(
name: Faker::Book.genre,
)
end
genres = Genre.all
Then run:
bin/rails db:reset
Next, we will generate a Genres
controller:
bin/rails g controller Genres
For now, we'll add only one method to this controller, the show
method:
def show
@genre = Genre.find(params[:id])
end
Let's add routes for the Genres
resources:
resources :genres
Let's create the view. In the app/views/genres/
folder, let's add the show.html.erb
file and
put this at the top:
<h1><%= @genre.name %></h1>
We will add a list of genres in the Books
index
view because it makes the most sense. Let's
make a @genres
variable available in the controller. Please edit the index
method like this:
def index
@books = Book.all
@genres = Genre.all
end
And, in the Books
index
view, after the books list, add this:
<h2>Genres</h2>
<ul>
<% @genres.each do |genre| %>
<li><%= link_to genre.name, genre %></li>
<% end %>
</ul>
Create the Association Between the Book and Genre Models
For these two models, we will use the has_and_belongs_to_many
association. A book can have multiple
genres, and a genre has multiple books. Let's edit our models to include this association. In the book.rb
file, add this:
has_and_belongs_to_many :genres
And in the genre.rb
file add this:
has_and_belongs_to_many :books
We need to create a join table. For this, we run the migration:
bin/rails generate migration CreateJoinTableBooksGenres books genres
This commmnad will generate a migration file to create the join table necessary for the many-to-many association between books and genres. Once you've generated the migration file, it will create a table that acts as the intermediary between books and genres, allowing the association to function. You can uncomment the methods to add indexes to the database, if you want. Then run:
bin/rails db:migrate
We will edit once again the seeds.rb
file to add genres to books. Edit the method as follows:
20.times do
book = Book.create(
title: Faker::Book.title,
publisher: Publisher.order("RANDOM()").first
)
authors_for_book = authors.sample(rand(1..3))
authors_for_book.each { |author| book.authors << author }
book.save if book.authors.present?
rand(1..3).times do
book.genres << genres.sample
end
end
We run:
bin/rails db:reset
Now, we can access the @book.genres
. In the Books
show
view, add this after the
publisher name:
<p>
<span>Genres </span>
<% @book.genres.each_with_index do |genre, index| %>
<% if index > 0 %>, <% end %>
<%= link_to genre.name, genre %>
<% end %>
</p>
In the Genres
show
view, after the h1
tag, add this:
<h2>Books</h2>
<ul>
<% @genre.books.each do |book| %>
<li><%= render partial: 'books/book', locals: {book: book} %></li>
<% end %>
</ul>
In the second part of this tutorial, we'll focus on creating, updating, and deleting records.