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.

  1. Open Terminal and go to your bookstore directory.

  2. Now, run rails generate model review.

    You might remember running this command when we created the books table.

    rails generate model review creates 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.yml
    

    The 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.rb
    

    Migration 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
  1. In your text editor, open the CreateReviews migration (db/migrations/TIMESTAMP_create_reviews.rb).

  2. Take a look at the change method. It has a create_table block that will create the reviews table.

    def change
      create_table :reviews do |t|
    
        t.timestamps
      end
    end
    

    If you ran the migration as it is now, your reviews table wouldn’t have anything other than the created_at and updated_at timestamps. We’ll need to change the create_table block so your new reviews table 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.

  1. Using your other migrations as an example, update the create_table block of your CreateReviews migration to add a text column called body to your reviews table.

  2. 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 your CreateReviews migration and re-run it with rake db:migrate.

  1. How did you update the migration?

    You needed to add one line to the create_table block.

    t.text :body
    
  2. If you didn’t already, add this line to your migration.

    If you ran the migration without adding the body column, 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.

  1. Go to Terminal and start the rails console.

  2. You now have a new object you can use: Review.

    Try running Review.new. It will return a new, empty Review.

    #<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.

  3. Using the Review class, create two reviews.

    The same methods you used to create books can be used to create reviews.

    When you’re done Review.count should 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>
  1. 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 new review and called save on it.

    a_new_review = Review.new
    a_new_review.body = "10/10 would recommend"
    a_new_review.save
    

    You might’ve even come up with something totally different!

    Somebody's impressed

  2. 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.

  1. In your text editor, open app/models/review.rb.

  2. Inside the Review class, add the following line.

    belongs_to :book
    

    With this change, we’re setting up your application so a Review belongs_to a Book.

  3. Save your changes.

1
2
3
  class Review < ApplicationRecord
    belongs_to :book
  end
  1. Go back to the rails console and run reload! to pick up your changes.

  2. Now, get your first review from the database and assign it to a variable called my_first_review.

    my_first_review = Review.first
    
  3. Let’s try to get my_first_review’s book.

    Run my_first_review.book.

    That returned nil because my_first_review was never associated with a book.

  4. Let’s try assigning your first book to your first review.

    Run the following:

    my_first_review.book = Book.first
    

    That returned an error, didn’t it? 🙃

  5. 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_to relationship between review and book, but we need a way to save that relationship in your database. To do that, we’ll need to add a book_id column to your reviews table.

  6. Let’s make that change to the reviews table, but before you move on exit the rails 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`
  ...

  >> exit

To 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.

  1. In Terminal, run rails generate migration add_book_id_to_reviews.

    This will generate a new timestamped migration called AddBookIdToReviews.

  2. Do you remember when you added description to the books table?

    class AddDescriptionToBooks < ActiveRecord::Migration[5.0]
      def change
        add_column :books, :description, :text
      end
    end
    

    We’ll want to do something similiar to add the book_id column to the reviews table.

  3. Open the AddBookIdToReviews migration in your text editor (db/migrate/TIMESTAMP_add_book_id_to_reviews.rb).

  4. Using the AddDescriptionToBooks migration as an example, update the AddBookIdToReviews migration so a book_id column is added to the reviews table. The book_id column should be an integer column.

  $ rails generate migration add_book_id_to_reviews
        invoke  active_record
        create    db/migrate/20170104024335_add_book_id_to_reviews.rb
  1. What does your solution look like?

    Inside the change method, you should have the following line:

    add_column :reviews, :book_id, :integer
    

    This will add a book_id column to the reviews table, and the new column will be an integer column.

  2. 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
  1. Now go back to Terminal and run your migration!

    rake db:migrate
    
  2. If you pay close attention to the output, you can see the book_id column getting added to the reviews table.

  $ 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!

  1. Open rails console and 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
    
  2. 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 its book_id is 1 - your first book’s id.

  3. 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.count should 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???

Wink wink

Since a book can have many reviews, we can define a has_many relationship between books and reviews.

  1. In your text editor, open app/models/book.rb, and add the followling line inside the class

    has_many :reviews
    

    This will set up your application so a Book has_many Reviews.

    In addition, we won’t have to make any database changes because the reviews table already has a book_id column. The relationship is already defined in the database.

  2. Save your changes.

1
2
3
  class Book < ApplicationRecord
    has_many :reviews
  end
  1. Now, go back to the rails console and run reload! to get you newest changes.

  2. Get your first book and assign it to my_first_book.

  3. Now, try running my_first_book.reviews.

    It might be hard to see, but all your book reviews are there!

    Dancing Banana

  4. Don’t believe me??

    Try running my_first_book.reviews.count. It should return 3.

  5. Spend some time playing around with this relationship. You can do things like my_first_book.reviews.first or my_first_book.reviews.pluck(:body).

    To find a complete list of all the things you can do, take a look at the documentation here.

  6. 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

  >> exit

The 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.

Data Table showing relationship