Ruby on Rails Models Association Basics - Let's Build a Bookstore App - Second Part
In Ruby on Rails Jan 2, 2024
Updated on Jan 4, 2024
Building on the foundation laid in the first part of our tutorial, we're poised to explore the dynamic functionalities of our bookstore application. In this segment, we'll journey further into the realm of Active Record associations, focusing on the practical implementation of crucial operations: creating, updating, and deleting records. We'll step through the process of enabling seamless interaction within our bookstore application. From adding new authors, publishers, and genres to creating and managing books, this section will empower you to wield the power of Rails associations effectively. Let's continue our exploration and dive deeper into the heart of Ruby on Rails associations.
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
Create and Update publishers
First, we will implement the ability to create and update publishers.
Let's start by adding a method that defines the strong parameters. At the bottom of the Publishers
controller, add this:
private
def publisher_params
params.require(:publisher).permit(:name)
end
Let's add the new
and create
methods:
def new
@publisher = Publisher.new
end
def create
@publisher = Publisher.new(publisher_params)
if @publisher.save
redirect_to publisher_url(@publisher)
else
render :new, status: :unprocessable_entity
end
end
In the app/views/publishers/
folder, we'll add a _form.html.erb
partial, since we will use
the same form for updating a publisher. Let's edit this partial:
<%= form_with(model: publisher) do |form| %>
<% if publisher.errors.any? %>
<div style="color: red">
<h2><%= pluralize(publisher.errors.count, "error") %> prohibited this publisher from being saved:</h2>
<ul>
<% publisher.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<p><%= form.label :name %></p>
<p><%= form.text_field :name %></p>
<p><%= form.submit %></p>
<% end %>
Next, we create a new
view, and add this partial to it:
<h1>New Publisher</h1>
<%= render "form", publisher: @publisher %>
Finally, we will link to the new_publisher_path
in the Publishers
index
view.
Below the h1
tag, add this:
<p><%= link_to "Add Publisher", new_publisher_path %></p>
Let's now implement the ability to update publisher records in the database. To facilitate this, we'll first establish
a method, set_publisher
, responsible for retrieving the specific publisher from the database based on its
ID:
def set_publisher
@publisher = Publisher.find(params[:id])
end
Next, we'll invoke this method using a before_action
filter at the top of the controller to ensure that
it fetches the publisher data before executing specific actions. In our case, it will be used for all actions,
excluding index
, new
, and create
:
before_action :set_publisher, except: [:index, :new, :create]
You can now remove the line to fetch the publisher in the show
action, as the
before_action :set_publisher
filter ensures this action already fetches the necessary data.
For the editing functionality, we'll create the edit
and update
methods in the controller:
def edit
end
def update
if @publisher.update(publisher_params)
redirect_to @publisher
else
render :edit, status: :unprocessable_entity
end
end
Now, to provide a seamless editing experience, we'll introduce an edit
view within the
app/views/publishers/
folder, incorporating the existing form partial:
<h1>Edit <%= @publisher.name %></h1>
<%= render "form", publisher: @publisher %>
In the show
view, add a link to the edit_publisher_path
after the books list:
<p>
<%= link_to 'Edit Publisher', edit_publisher_path(@publisher) %>
</p>
CREATE GENRES
Next, we'll implement the ability to add genres. First, let's define the strong parameters in the private method
genre_params
:
private
def genre_params
params.require(:genre).permit(:name)
end
And let's add the new
and create
methods, as well:
def new
@genre = Genre.new
end
def create
@genre = Genre.new(genre_params)
if @genre.save
redirect_to root_path
else
render :new, status: :unprocessable_entity
end
end
Next, we will create a new.html.erb
file in the app/views/genres/
folder, and edit it like
this:
<%= form_with(model: @genre) do |form| %>
<% if @genre.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@genre.errors.count, "error") %> prohibited this genre from being saved:</h2>
<ul>
<% @genre.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<p><%= form.label :name %></p>
<p><%= form.text_field :name %></p>
<p><%= form.submit %></p>
<% end %>
And, in the books
index
view, we'll add a link to the new_genre_path
, before
the genres list:
<%= link_to "Add Genre", new_genre_path %>
CREATE AND EDIT AUTHORS
Let's now implement the ability to add a new author. When we add or update an author, we automatically want to be able
to add or edit their biography, too. For this, we will use nested attributes. Nested attributes allow you to save
attributes on associated models through the parent.
To enable these nested attributes, we will use the accepts_nested_attributes_for
method. Let's add this
method to the app/model.author.rb
file:
accepts_nested_attributes_for :biography
Enabling nested attributes simplifies the process of creating or updating associated records (like the biography) alongside the parent record (author) in a single action.
Let's validate the content attribute in the Biography
model. Let's add this in the
app/models/biography.rb
file:
validates :content, presence: true, length: { minimum: 10 }
This ensures that these validations are enforced when creating or updating an author and their biography. If any validation fails during the save operation, the error messages propagate back to the form, allowing users to correct errors.
We now need to edit our Authors
controller to add the new and the create methods. But first, let's add at
the bottom, the private method author_params
that defines the strong parameters:
private
def author_params
params.require(:author).permit(:name, biography_attributes: :content)
end
Now, let's add the new
and the create
methods:
def new
@author = Author.new
@biography = @author.build_biography
end
We are initializing a new Author
object and building an associated Biography
object, which
aligns perfectly with the nested attributes setup. This allows us to create an author and their biography in a single
action.
def create
@author = Author.new(author_params)
if @author.save
redirect_to author_url(@author)
else
render :new, status: :unprocessable_entity
end
end
In the app/views/authors/
folder, we'll add a _form.html.erb
partial. Let's edit this
partial:
<%= form_with(model: author) do |form| %>
<% if author.errors.any? %>
<div style="color: red">
<h2><%= pluralize(author.errors.count, "error") %> prohibited this author from being saved:</h2>
<ul>
<% author.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<p><%= form.label :name %></p>
<p><%= form.text_field :name %></p>
<%= form.fields_for :biography do |biography_fields| %>
<p><%= biography_fields.label :content, "Biography" %></p>
<p><%= biography_fields.text_area :content %></p>
<% end %>
<p><%= form.submit %></p>
<% end %>
Next, we create a new
view, and add this partial to it:
<h1>New Author</h1>
<%= render "form", author: @author %>
Finally, we will link to the new_author_path
in the Authors
index
view. Below
the h1
tag, add this:
<p><%= link_to "Add Author", new_author_path %></p>
Let's enable the modification of author records within our application by introducing an editing feature. Similar to
the process with the publisher, we'll begin by creating a set_author
method in the controller to retrieve
an author from the database based on its ID
:
def set_author
@author = Author.find(params[:id])
end
Following our previous approach, we'll utilize a before_action
filter to invoke this method for specific
actions, excluding index
, new
, and create
:
before_action :set_author, except: [:index, :new, :create]
You can now delete the line to fetch the author in the show
method.
Now, to facilitate the editing functionality, we'll define the edit
and update
methods in
the controller:
def edit
end
def update
if @author.update(author_params)
redirect_to @author
else
render :new, status: :unprocessable_entity
end
end
To complement this functionality, let's create an edit
view within the app/views/authors/
folder, utilizing the existing form partial:
<h1>Edit <%= @author.name %></h1>
<%= render "form", author: @author %>
Additionally, in the show
view, we'll include, after the author book's list, a link directing users to
the author's edit path, enhancing the navigational experience:
<p>
<%= link_to 'Edit Author', edit_author_path(@author) %>
</p>
When updating an author, Rails will attempt to delete and create a new biography associated with that author. But if
you want only to update it, you must use the :update_only
option on
accepts_nested_attributes_for
class method. Let's modify it in app/models/author.rb
file:
accepts_nested_attributes_for :biography, update_only: true
EDIT THE BOOK FORM
Let's now edit the books form to include fields for authors, publishers, and genres. First, we'll have to edit the
book_params
method that sets the strong parameters:
def book_params
params.require(:book).permit(:title, :publisher_id, author_ids: [], genre_ids: [])
end
We've included the publisher_id
, author_ids
array, and the genres_ids
array.
The form that was generated when we created the book's scaffold includes only the title field. We will have to include fields for the book author (or authors), the genre, and the publisher.
Let's start with the publisher. We will use the collection_select
helper. Let's add this to our form:
<p>
<%= form.label :publisher_id, style: "display: block" %>
<%= form.collection_select :publisher_id, Publisher.all, :id, :name, { prompt: true } %>
</p>
The collection_select
helper in Rails generates a dropdown select element within a form. It's
particularly useful when you want users to choose a single item from a collection. In the context of our book creation
form, collection_select
allows us to select a single publisher for a book. Here's a breakdown of its
parameters:
:publisher_id
specifies the attribute in the Book
model that will store the selected
publisher's ID
.
Publisher.all
represents the collection of all publishers available in the database.
:id
is the method called on each Publisher
object to retrieve its ID
.
:name
is the method called on each Publisher
object to retrieve its name (displayed in the
dropdown).
{ prompt: true }
adds a default prompt (e.g., "Select a publisher") as the first option in the dropdown.
For the authors, we'll use checkboxes, since a book can have multiple authors. collection_check_boxes
generates a set of checkboxes within a form, allowing users to select multiple items from a collection. It's ideal
when dealing with associations that allow multiple selections, like books having multiple authors or belonging to
multiple genres.
Let's add this to the form:
<fieldset>
<legend>Authors</legend>
<%= form.collection_check_boxes :author_ids, Author.all, :id, :name %>
</fieldset>
Here's an explanation of its usage:
:author_ids
specifies the attribute in the Book
model that will store the selected authors'
ID
s. It's an array because a book can have multiple authors.
Author.all
represents the collection of all authors available in the database.
:id
retrieves the ID
of each Author
object.
:name
retrieves the name of each Author
object, which is displayed alongside the checkboxes.
We will use the collection_select
helper to generate fields for adding the book's genres as well:
<fieldset>
<legend>Genres</legend>
<%= form.collection_check_boxes :genre_ids, Genre.all, :id, :name %>
</fieldset>
Similarly, the collection_check_boxes
for genres functions in the same way, allowing the selection of
multiple genres for a book.
By incorporating these helpers into our form, users can efficiently select publishers, authors, and genres for a book during creation, reflecting the relationships between books and their associated entities in the database.
DELETE BOOKS
One last thing we need to do. When attempting to delete a book, if there are associated records in the
book_authors
table, direct deletion becomes problematic due to referential integrity. To enable the
deletion of books along with their associated authors in the book_authors
table, we use
dependent: :destroy
in the has_many :book_authors
association within the Book
model. This setup ensures that when a book is deleted, its associated book-author relationships are also removed,
maintaining data consistency.
So, let's edit this association in the app/models/book.rb
file:
has_many :authors, through: :book_authors, dependent: :destroy
Conversely, for associations like has_and_belongs_to_many :genres
, Rails applies a default behavior that
automatically handles the deletion of associated records in join tables, such as the books_genres
table.
Therefore, when a book is deleted, its associations with genres in the join table are automatically removed without
the need for explicit dependent options.
conclusion
In this tutorial, we've explored the backbone of Ruby on Rails development: Active Record associations. We've navigated through these types of associations, understanding their roles and intricacies while constructing a robust bookstore application using the latest version of Ruby on Rails.