Reviewing a Book
Your bookstore is full of wonderful books. Although they have some great descriptions, your books deserve great advocates!
Your books need reviews. 😉
Before we can add reviews, we need a place to store them. Let’s add a new table to your database.
-
Open Terminal and go to your
bookstoredirectory. -
Now, run
rails generate model review.You might remember running this command when we created the books table.
rails generate model reviewcreates a bunch of files.invoke active_record create db/migrate/20170103220053_create_reviews.rb create app/models/review.rb invoke test_unit create test/models/review_test.rb create test/fixtures/reviews.ymlThe first file it creates is a migration file. We’ve already created a few of these in your bookstore application.
$ ls -l db/migrate total 24 -rw-r--r-- 1 alimi staff 230 Dec 30 12:00 20161115030350_create_books.rb -rw-r--r-- 1 alimi staff 125 Dec 30 12:00 20161227020938_add_description_to_books.rbMigration files always have file names that start with the timestamp of when they were created.
Let’s take a look at the migration that was generated by
rails generate model review.
$ pwd
/Users/awesomesauce/Projects/bookstore
$ rails generate model review
invoke active_record
create db/migrate/20170103220053_create_reviews.rb
create app/models/review.rb
invoke test_unit
create test/models/review_test.rb
create test/fixtures/reviews.yml-
In your text editor, open the
CreateReviewsmigration (db/migrations/TIMESTAMP_create_reviews.rb). -
Take a look at the
changemethod. It has acreate_tableblock that will create thereviewstable.def change create_table :reviews do |t| t.timestamps end endIf you ran the migration as it is now, your
reviewstable wouldn’t have anything other than thecreated_atandupdated_attimestamps. We’ll need to change thecreate_tableblock so your newreviewstable can store more review related data. 😄
1
2
3
4
5
6
7
8
class CreateReviews < ActiveRecord::Migration[5.0]
def change
create_table :reviews do |t|
t.timestamps
end
end
end
The reviews table will need a column to store the text of a review. Let’s call that column body, and let’s make it a text column so you can leave long, inspiring reviews.
-
Using your other migrations as an example, update the
create_tableblock of yourCreateReviewsmigration to add atextcolumn calledbodyto yourreviewstable. -
When you’re done, save your changes, and run the migration by running
rake db:migrate.If you make any mistakes, you can always undo the migration by running
rake db:rollback. Then, you can make changes to yourCreateReviewsmigration and re-run it withrake db:migrate.
-
How did you update the migration?
You needed to add one line to the
create_tableblock.t.text :body -
If you didn’t already, add this line to your migration.
If you ran the migration without adding the
bodycolumn, you’ll need to rollback and re-run the migration.
1
2
3
4
5
6
7
8
9
class CreateReviews < ActiveRecord::Migration[5.0]
def change
create_table :reviews do |t|
t.text :body
t.timestamps
end
end
end
Now that we have reviews, let’s play with them on the rails console.
-
Go to Terminal and start the
rails console. -
You now have a new object you can use:
Review.Try running
Review.new. It will return a new, emptyReview.#<Review id: nil, body: nil, created_at: nil, updated_at: nil>In addition to id, created_at, and updated_at, the new review also has a body.
-
Using the
Reviewclass, create two reviews.The same methods you used to create books can be used to create reviews.
When you’re done
Review.countshould return 2.
$ rails console
Loading development environment (Rails 5.0.0.1)
>> Review.new
=> #<Review id: nil, body: nil, created_at: nil, updated_at: nil>-
Did you create two reviews? How did you do it?
Maybe you created a review by using
create.Review.create(body: "Wow, what a great book!")Or maybe you started with a
newreview and calledsaveon it.a_new_review = Review.new a_new_review.body = "10/10 would recommend" a_new_review.saveYou might’ve even come up with something totally different!

-
If you get 2 when you run
Review.count, yay!If not, use either of the methods we described to create two reviews.
Associating Reviews to Books
Now you have a couple of reviews, but doesn’t it feel like we’re missing something?
How are these reviews related to books?
They’re not 😅
Fortunately, Rails gives us a few tools to associate reviews to books.
In this case, we can say a review should only belong to one book. This is called a belongs_to relationship.
Let’s define the belongs_to relationship.
-
In your text editor, open
app/models/review.rb. -
Inside the
Reviewclass, add the following line.belongs_to :bookWith this change, we’re setting up your application so a
Reviewbelongs_toaBook. -
Save your changes.
1
2
3
class Review < ApplicationRecord
belongs_to :book
end
-
Go back to the
rails consoleand runreload!to pick up your changes. -
Now, get your first review from the database and assign it to a variable called
my_first_review.my_first_review = Review.first -
Let’s try to get
my_first_review’s book.Run
my_first_review.book.That returned nil because
my_first_reviewwas never associated with a book. -
Let’s try assigning your first book to your first review.
Run the following:
my_first_review.book = Book.firstThat returned an error, didn’t it? 🙃
-
It might be hard to see, but you actually got a pretty useful error…
ActiveModel::MissingAttributeError: can't write unknown attribute `book_id`We’ve defined the
belongs_torelationship between review and book, but we need a way to save that relationship in your database. To do that, we’ll need to add abook_idcolumn to yourreviewstable. -
Let’s make that change to the
reviewstable, but before you move on exit therails console.
>> reload!
Reloading...
=> true
>> my_first_review = Review.first
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" ORDER BY "reviews"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Review id: 1, body: "Wow, what a great book!", created_at: "2017-01-04 02:07:50", updated_at: "2017-01-04 02:07:50">
>> my_first_review.book
=> nil
>> my_first_review.book = Book.first
Book Load (0.3ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? [["LIMIT", 1]]
ActiveModel::MissingAttributeError: can't write unknown attribute `book_id`
...
>> exitTo add a book_id column to the reviews table, we’ll need a new migration.
Remember, editing an old migration would mean rolling back that migration and we don’t want to do that because that would delete the data in the database.
-
In Terminal, run
rails generate migration add_book_id_to_reviews.This will generate a new timestamped migration called
AddBookIdToReviews. -
Do you remember when you added
descriptionto thebookstable?class AddDescriptionToBooks < ActiveRecord::Migration[5.0] def change add_column :books, :description, :text end endWe’ll want to do something similiar to add the
book_idcolumn to thereviewstable. -
Open the
AddBookIdToReviewsmigration in your text editor (db/migrate/TIMESTAMP_add_book_id_to_reviews.rb). -
Using the
AddDescriptionToBooksmigration as an example, update theAddBookIdToReviewsmigration so abook_idcolumn is added to thereviewstable. Thebook_idcolumn should be anintegercolumn.
$ rails generate migration add_book_id_to_reviews
invoke active_record
create db/migrate/20170104024335_add_book_id_to_reviews.rb-
What does your solution look like?
Inside the
changemethod, you should have the following line:add_column :reviews, :book_id, :integerThis will add a
book_idcolumn to thereviewstable, and the new column will be an integer column. -
Update your solution to match this solution and save your changes.
1
2
3
4
5
class AddBookIdToReviews < ActiveRecord::Migration[5.0]
def change
add_column :reviews, :book_id, :integer
end
end
-
Now go back to Terminal and run your migration!
rake db:migrate -
If you pay close attention to the output, you can see the
book_idcolumn getting added to thereviewstable.
$ rake db:migrate
== 20170104024335 AddBookIdToReviews: migrating ===============================
-- add_column(:reviews, :book_id, :integer)
-> 0.0073s
== 20170104024335 AddBookIdToReviews: migrated (0.0074s) ======================Now that the reviews table has a book_id column, you can assign books to reviews. Let’s try it!
-
Open
rails consoleand run the following code to assign your first book to your first review.my_first_review = Review.first my_first_review.book = Book.first my_first_review.save -
Now, try running
my_first_review.book.Yay! 🎉
Your first review is now associated with your first book.
If you look at
my_first_review, you’ll notice itsbook_idis 1 - your first book’s id. -
Go ahead and assign your second review to your first book. While you’re at it, also create another review for your first book.
When you’re done,
Review.countshould return 3.
$ rails console
Loading development environment (Rails 5.0.0.1)
>> my_first_review = Review.first
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" ORDER BY "reviews"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Review id: 1, body: "Wow, what a great book!", created_at: "2017-01-04 02:07:50", updated_at: "2017-01-04 02:07:50", book_id: nil>
>> my_first_review.book = Book.first
Book Load (0.7ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Book id: 1, title: "why's (poignant) Guide to Ruby", author: "why the lucky stiff", price_cents: 100, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-30 20:29:14", quantity: 500, description: "Chunky Bacon!">
>> my_first_review.save
(0.1ms) begin transaction
SQL (0.8ms) UPDATE "reviews" SET "updated_at" = ?, "book_id" = ? WHERE "reviews"."id" = ? [["updated_at", 2017-01-04 03:14:40 UTC], ["book_id", 1], ["id", 1]]
(3.2ms) commit transaction
=> true
>> my_first_review.book
=> #<Book id: 1, title: "why's (poignant) Guide to Ruby", author: "why the lucky stiff", price_cents: 100, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-30 20:29:14", quantity: 500, description: "Chunky Bacon!">A Book Has Many Reviews
Your first book now has multiple reviews.
We can find which review a book belongs to, but we can’t find all the reviews a book has.
Or can we???

Since a book can have many reviews, we can define a has_many relationship between books and reviews.
-
In your text editor, open
app/models/book.rb, and add the followling line inside the classhas_many :reviewsThis will set up your application so a
Bookhas_manyReviews.In addition, we won’t have to make any database changes because the
reviewstable already has abook_idcolumn. The relationship is already defined in the database. -
Save your changes.
1
2
3
class Book < ApplicationRecord
has_many :reviews
end
-
Now, go back to the
rails consoleand runreload!to get you newest changes. -
Get your first book and assign it to
my_first_book. -
Now, try running
my_first_book.reviews.It might be hard to see, but all your book reviews are there!

-
Don’t believe me??
Try running
my_first_book.reviews.count. It should return 3. -
Spend some time playing around with this relationship. You can do things like
my_first_book.reviews.firstormy_first_book.reviews.pluck(:body).To find a complete list of all the things you can do, take a look at the documentation here.
-
When your done exploring, exit the
rails console.
>> reload!
Reloading...
=> true
>> my_first_book = Book.first
Book Load (0.1ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Book id: 1, title: "why's (poignant) Guide to Ruby", author: "why the lucky stiff", price_cents: 100, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-30 20:29:14", quantity: 500, description: "Chunky Bacon!">
>> my_first_book.reviews
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."book_id" = ? [["book_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Review id: 1, body: "Wow, what a great book!", created_at: "2017-01-04 02:07:50", updated_at: "2017-01-04 03:14:40", book_id: 1>, #<Review id: 2, body: "10/10 would recommend", created_at: "2017-01-04 02:08:57", updated_at: "2017-01-04 03:23:23", book_id: 1>, #<Review id: 3, body: "This book is so good!", created_at: "2017-01-04 03:23:12", updated_at: "2017-01-04 03:23:12", book_id: 1>]>
>> my_first_book.reviews.count
(0.4ms) SELECT COUNT(*) FROM "reviews" WHERE "reviews"."book_id" = ? [["book_id", 1]]
=> 3
>> exitThe magic that is a Relational Database!
Are you starting to see how we’re using Rails to build a relational database?
So far, you’ve built two tables: a books table and a reviews table. Those tables are connected to one another by the book_id column on the Reviews table. That column is making it possible for books to have many reviews in our database.
