Building a Reading Log App with the Google Books API and Ruby on Rails - Part 2

Setting up a Postgresql database and validating records

In my previous post, we made our way through the process of successfully making an API call to Google's Books API from a new Rails app and verifying that a successful response is returned. ✅ If you're like me, simply understanding how to fetch data from an external API is astounding! But we're not stopping there. The next challenge in the development of our reading log is twofold: ☝️ correctly configure a deployment-ready database system, and ✌️ ensure that we can create and persist records for Books and Shelves, including shelves for:

  • 📚 we want to read,
  • 📚 we are currently reading,
  • 📚 we have read, and
  • 📚 that hold a special place among our absolute favorites!

Though it might be tempting to assume that our next step should be building out a user-interface, life will be a lot easier if we prioritize architecting our database and table schemas first.

By default, newly generated Rails apps (like this one) come with built-in support for a SQLite database. While SQLite is fine for development, in a later article in this series we will walk through deploying our reading log app with Heroku. As a prerequisite, we need to uninstall SQLite and configure our app to use Postgresql, which is another type of relational database system similar to SQLite.

Replacing SQLite with Postgresql

Let's begin in our app's Gemfile. 💎

Here, all we need to do is delete:

# Use sqlite3 as the database for Active Record
gem "sqlite3"

and replace it with:

# Use Postgres as the database for Active Record
gem "pg"

Once that's done, run bundle install in your command line to uninstall sqlite3 and install pg. You should see output similar to the following (though your gems' version numbers might be different than mine appear here):

Installing pg 1.3.5 with native extensions
Updating files in vendor/cache
  * pg-1.3.5.gem
Removing outdated .gem files from vendor/cache
  * sqlite3-1.4.2.gem
Bundle complete! 17 Gemfile dependencies, 77 gems now installed.
Bundled gems are installed into `./vendor/cache`

You can additionally verify this change by looking at your Gemfile.lock after the bundle update completes.

Next, we need to replace the contents of our config/database.yml file with the following configuration (note: replace reading_log_tutorial_* with the name of your own app):

development:
  adapter: postgresql
  database: reading_log_tutorial_development
  pool: 5
  timeout: 5000
test:
  adapter: postgresql
  database: reading_log_tutorial_test
  pool: 5
  timeout: 5000

production:
  adapter: postgresql
  database: reading_log_tutorial_production
  pool: 5
  timeout: 5000

Lastly, let's setup our new Postgresql database by running the following command in the command line:

% rake db:setup

If successful, you should see a message similar to the following towards the end of the output:

Created database 'reading_log_tutorial_development'
Created database 'reading_log_tutorial_test'

Defining Table Schemas & Scaffolding Resources

😅 Now that we have setup our app with a production-ready database system, we can define and create the tables we need for storing records in that database. Since we intend to have books that can be organized by shelves, we will need a table for books and a separate table for shelves.

As we've seen, the response we receive from the Books API provides a lot of data for any given book (identified by the "volumeInfo" key). In addition to viewing the returned book fields in our Rails console, we can also see them in the API's documentation. For our app, let's have each book we store include the book's:

  • title (as a string),
  • authors (as a hash of strings),
  • description (as text),
  • page count (as an integer),
  • categories (as a hash of strings), and
  • image links (as a hash of strings)

And for each shelf we store, let's include the shelf's:

  • name (as a string)

One of the things I ❤️ most about the Ruby on Rails framework is how easy it makes seemingly difficult tasks, like creating the migration files we need for migrating our database tables, as well as automatically generating our needed routes, controllers, and views associated with the resources we mapped out above.

We can do this by executing the following Rails scaffold commands:

rails generate scaffold Book title:string authors:jsonb description:text page_count:integer categories:jsonb image_links:jsonb

and

rails generate scaffold Shelf name:string

💥 We now have all the files we need for our Books and Shelves. . .

But if you look at our db/schema.rb file, you'll notice that we don't yet have a books table or a shelves table. 🤷‍♂️ To create these, we need to tell Rails to build them based on the contents of our db/migrate directory. This is done by executing the following in our command line:

rake db:migrate

If done successfully, you should see output similar to:

== 20220422041449 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0159s
== 20220422041449 CreateBooks: migrated (0.0160s) =============================

== 20220422041601 CreateShelves: migrating ====================================
-- create_table(:shelves)
   -> 0.0052s
== 20220422041601 CreateShelves: migrated (0.0053s) ===========================

💁 Note: If you are not yet familiar with Rails' scaffolding process, take a moment to look at the auto-generated contents of your app's config/routes.rb file along with the app/controllers, app/helpers, app/models, and app/views directories. 🤯

Persisting Books & Shelves in Our Database

Our reading log app is now to the point where we can create new records for both books and shelves and persist them in our database through a Rails console. Let's do it!

After starting a new Rails console in the command line, create a book record by executing the following:

Book.create!(:title => "Hello Book World", :authors => ["Seymour Butts"])

🎉 We created our first book!

 =>
#<Book:0x00007fadd0157db0
 id: 1,
 title: "Hello Book World",
 authors: "Seymour Butts",
 description: nil,
 page_count: nil,
 categories: nil,
 image_links: nil,
 created_at: Fri, 22 Apr 2022 04:50:57.295025000 UTC +00:00,
 updated_at: Fri, 22 Apr 2022 04:50:57.295025000 UTC +00:00>

What do you notice about our books attribute values? 🤔

We have values for a title and an author. But we have nil for description, page_count, categories, and image_links.

Let's make sure that whenever we create a new book record in our database that it validates that a value is present for each of our book's attributes by adding validations to our /models/book.rb file, like so:

class Book < ApplicationRecord
  validates_presence_of :title, :authors, :description, :page_count, :categories, :image_links
end

Now, after restarting our Rails console, if we attempt to create a new book record with incomplete data, the action will fail and the output will contain a validation error message that looks something like this:

Validation failed: Description can't be blank, Page count can't be blank, Categories can't be blank, Image links can't be blank (ActiveRecord::RecordInvalid)

If we attempt to create a new book and provide values for all of the fields we now validate to be present, our database transaction succeeds. Check it out!

> Book.create!(:title => "Hello Book World, the Sequel", :authors => ["Harry Butts"], :description => "A better story than the last", :page_count => 123,
 :categories => ["Programming"], :image_links => { :thumbnail => "www.notathumbnail.com" })

The output verifies a successful database transaction.

  TRANSACTION (0.1ms)  BEGIN
  Book Create (1.4ms)  INSERT INTO "books" ("title", "authors", "description", "page_count", "categories", "image_links", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id"  [["title", "Hello Book World, the Sequel"], ["authors", "[\"Harry Butts\"]"], ["description", "A better story than the last"], ["page_count", 123], ["categories", "[\"Programming\"]"], ["image_links", "{\"thumbnail\":\"www.notathumbnail.com\"}"], ["created_at", "2022-04-23 04:59:55.634131"], ["updated_at", "2022-04-23 04:59:55.634131"]]
  TRANSACTION (0.5ms)  COMMIT
 => 
#<Book:0x00007fefed5dcbb0
 id: 1,
 title: "Hello Book World, the Sequel",
 authors: ["Harry Butts"],
 description: "A better story than the last",
 page_count: 123,
 categories: ["Programming"],
 image_links: {"thumbnail"=>"www.notathumbnail.com"},
 created_at: Sat, 23 Apr 2022 04:59:55.634131000 UTC +00:00,
 updated_at: Sat, 23 Apr 2022 04:59:55.634131000 UTC +00:00>

Woot! 🙌

Before we forget, let's go add validation to models/shelf.rb to make sure a value for the name attribute is present on newly created shelf records, just like we do with our book attributes.

class Shelf < ApplicationRecord
  validates_presence_of :name
end

After testing in a restarted Rails console, we see that we can now create and persist shelves too!

> Shelf.create!(:name => "To Be Read")
  TRANSACTION (0.2ms)  BEGIN
  Shelf Create (1.1ms)  INSERT INTO "shelves" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "To Be Read"], ["created_at", "2022-04-23 05:01:14.768796"], ["updated_at", "2022-04-23 05:01:14.768796"]]
  TRANSACTION (0.3ms)  COMMIT
 => 
#<Shelf:0x00007fefecc2e758
 id: 1,
 name: "To Be Read",
 created_at: Sat, 23 Apr 2022 05:01:14.768796000 UTC +00:00,
 updated_at: Sat, 23 Apr 2022 05:01:14.768796000 UTC +00:00>

Hotdog! We've made a lot of progress. Together, we learned how to:

  1. Replace SQLite with Postgres as our database system for production-ready deploys,

  2. Define table schemas,

  3. Create scaffolded resources, including files for migrations, models, controllers, and views, and

  4. Add validations to our model classes to ensure that we only persist records to our database that have values present for each attribute.

In part 3️⃣ of this series, we'll learn how to organize books by shelves through creating model associations and a third unique database table commonly known as a bridge table.

In part 4️⃣, we'll dive into our views and controllers to create book records from our fetched API data with the click of a button. Stay tuned!