Creating Your Own Back-end and API with Ruby [Part 1] — Migrations with Active Record
Building a full stack application with your own custom API through Sinatra and ActiveRecord
Agenda
- Project Review
- Basic overview of Ruby Object-Relational Mapping
- Ruby’s Active Record gem
- Starter code for project
- Creating migrations in Active Record
- Creating models in Active Record
Getting Started — Project Review
For a recent project, I completed a full-stack Single Page Application (API) for tracking books and reviews. The goal was to flex my skills in building a back-end database and API through Ruby. It was my most challenging project to date but a fulfilling one nonetheless! The posts I will make for this project outline the back-end build-out. The front-end of this project runs on VanillaJS and React.js. Feel free to check out all the code for that on my project’s GitHub repository!
Object-Relational Mapping
Working with databases is a requirement for just about any complex application. Building databases can be incredibly cumbersome, requiring you to leverage multiple languages and sometimes very lengthy scripts to get the necessary data. There are dozens of ways you can interact with databases, but the one we will talk about today is Object-Relational Mapping or ORM. Flatiron School explains ORM as the following:
Object-Relational Mapping (ORM) is the technique of accessing a relational database using an object-oriented programming language. Object Relational Mapping is a way … to manage database data by “mapping” database tables to classes and instances of classes to rows in those tables.
Simply put, we will use a programming language to interact with a database. You can use any OO language, so I used Ruby. Leveraging ORM helps to reduce duplicative code and make it more dynamic. While we can use regular Ruby to interact with the database, that would require us to write code (sometimes a lot) to create a connection to the database and send SQL statements for CRUD functionality. Not impossible, but luckily the lazy developer mentality sparked the initiative to create a Ruby gem that allows you to create an ORM without having to write out your own.
Ruby and Active Record
Active Record is a gem for Ruby that acts as an ORM (object-relational mapper). This gem allows us the same functionality of an ORM, like creating classes that store and interact with databases, without having to build all of it ourselves. We’re leveraging shortcuts without needing to recreate the wheel every time we have to interact with a new database.
Starting Code
Flatiron has created a base. repository for a back-end code setup with all the files we need to start the back-end build-out process. Check it out with this repository.
Note — I’m writing another post about working with Sinatra, a Ruby gem that allows you to create an API for your database. After all, what good is creating a database if you can’t talk to it from an application? I will focus on creating database migrations, or connections, through Active Record for the rest of the post.
Before continuing, you can open the project repository and run this in the terminal
## Installs required gems
bundle install
Required Gems (included in the starter repository)
- Rake — Automate and run common tasks from the command line
- Active Record — Creates an ORM within Ruby
- Sinatra — Domain-specific language for creating web applications
- Sinatra Active Record — Configures common Rake tasks for working with Active Record
- Rack-CORS — Rack middleware that allows your front-end to communicate with your back-end
- Sqlite3 — Provides functionality to interact with a SQLite3 database. Ruby supports other database wrappers, so you can use what you’re comfortable with!
Creating Migrations with Active Record
If you’re solely using Ruby and creating an ORM without Active Record, you would need to write and execute SQL code to build a connection with the database. With Active Record, you wouldn’t need to write any SQL. Those connections go through “migrations” where Active Record creates and modifies the database table(s).
An important thing to note — Active Record (and Ruby on Rails) follows the ‘Convention over Configuration’ paradigm, so there is built-in logic that works on the back-end as long as you follow its conventions. This setup is meant for speed and workload reduction but can cause errors if you don’t follow the conventions. I’ll work through those as we go along!
To start, we’re going to need to create migrations. Remember, this project is about a book/review tracking website. So we need to know how we want this database to look before building the migrations. To help, I’ve created a visual for this through DB Diagram.
There is always an opportunity to add more data points to our database though I thought this was adequate to start. Notice the relationships between the tables as well (relationships make databases powerful). We have a few different one-to-many relationships happening.
- Each book has one author, and each author can have many books
- Each review has one book, and each book can have many reviews
As such, we must ensure that every table has a key connecting it to its relational partner. Wherever there is a one-to-many relationship, the foreign key should exist in the “many” table (i.e. an author can have many books, but each book only has one other, so there should be an author_id key in the books database).
So let’s start to create the database tables. The first will be the “books” table. In the command line, type in this command:
bundle exec rake db:create_migration NAME=create_books
We won’t go into too much detail here about Rake, but it is a gem that allows you to run commands from the command line that execute code for common tasks. Rails/Active Record includes built-in rake commands that allow you to create migrations, execute migrations, and seed data.
Notice the NAME=create_books
. The name create_books
is a description for the migration, but we are using this to create the “books” table.
After you run this command, you should see a file auto-populate in the db>migration folder with a timestamp and the name of the migration you just created (i.e. 20221001164733_create_books.rb). DO NOT EDIT THIS NAMING CONVENTION. Active Record runs all of these files in order alphanumeric when building the database, so the timestamp is attached so that you always open the files in their creation order. If you create another migration that edits a previously created table (as we’ll see shortly), this will ensure that the table is created first before it runs the migration to edit it.
Let’s open up the migration file in db>migration (i.e. 20221001164733_create_books.rb) and create our table. Here’s the code for creating that:
Note — don’t change the name of the class name “CreateBooks” as this name needs to make the migration name (Active Record does a lot of the heavy lifting, we have to not mess with it). Also — don’t create an “ID” line item. IDs are generated automatically on the back-end using some magic to ensure they are all unique.
Let’s create the other migrations with the same conventions before we submit the migrations. You can submit the following commands on two different lines in the terminal.
bundle exec rake db:create_migration NAME=create_reviewsbundle exec rake db:create_migration NAME=create_authors
And here’s what the code looks like in each of those files:
Perfect! These are looking great. Now that these have been created, let’s push these migrations through.
bundle exec rake db:migrate
After this runs, you should get a success method letting you know that everything is good to go. You can also run the following to check the individual statuses of each of the migrations:
bundle exec rake db:migrate:status
This looks great! But eventually, I uncovered a problem. I made the genre column a string, but I’d rather that be an array of genres rather than just one (after all, a book can have many genres). That way, if it’s an array, I can format them separately on the website. This setup is challenging because “Array” is not a valid data type for Active Record migrations. There’s a fix for this that I’ll review shortly to set the proper data type, but for now, I have to push through another migration. If you need to, you could pull back those migrations, change them, and push them forward again, but it’s not a good practice to edit anything on these migrations after they are live. Instead, we can create another migration to adjust the table.
bundle exec rake db:create_migration NAME=change_genre_to_genres_for_books
Here’s the code I used for that:
Again, there’s no “Array” data type in Rails, so instead, I removed the column and added a “genres” column (pluralized now for accuracy), and changed it to a text field. You can check out this StackOverflow thread explaining the differences between string and text data types on Rails.
Creating Models in Active Record
One of the last pieces is to create a model for each table in the database. Following the Model-View-Controller methodology, we must create “Models” to manage the application data directly and create the structure. Read more on this Wikipedia page about the Model-View-Controller architectural pattern. These are a required part of the project since we wouldn’t be able to create and leverage the data on the front-end without them.
For this project, we kept things relatively simple but they can become more intricate if you chose to build a lot of methods to interact with the data.
Models typically exist in a separate folder called “models” within an “app” folder in the repository. In the “models” folder, let’s create the files based on our application. Since we have tables for books, reviews, and authors, we’ll create the following files:
- Book.rb
- Author.rb
- Review.rb
These names are at random. They are specific to the convention of Active Record. See below for some of those details.
Conventions to Follow
- Class names need to be capitalized and singular (i.e. Book.rb)
- Migrations need to be lowercase and plural (i.e. create_books)
- Tables need to be lowercase and plural (i.e. table “:books”)
Within these models, we’ll also set up the relationships between the tables if there are any. Remember, we have connections between books <> authors and books <> reviews.
Here’s how the Book.rb model file looks:
From here, we can see that the class Book must be capitalized and singular, inherit from the ActiveRecord::Base class and identify the associations it has with other tables. If we say the Book class belongs_to :author
, it will use the “author_id” field that we created in the create_books migration and associate that with the matching author in the Author table. This means that we can keep our databases separate and well-defined while still pulling in applicable data from other databases that are connected.
Here’s the code for the Review and Author model files.
As we can see from the Author model, we added another method within the class. Since these are classes, we can create class and instance methods that can manipulate and return data. For example, with my project, if I deleted a book from the database but the author still showed, it looked a little off. So I built a class method to find all authors with no/empty books. I’ll eventually use that in the API to delete those easily from the database. We’ll get into that in the next post!
Wrapping Up
That takes us to the end of the first part! At this point, we understand object-relational mapping with our OO language of choice, Ruby, and have leveraged Ruby gems to make creating those database connections easier. Now that we’ve built the database, our next step is to create an API so that our front-end can interact directly with that database. Stay tuned, and until next time, happy coding!
Resources
- GitHub Repository for Book Tracking Project
- GitHub Repository for Starter Sinatra Code
- YouTube Video Walkthrough of Project
- Database Diagram Online Tool
- Wikipedia page about the Model-View-Controller architectural pattern
- StackOverflow thread explaining the differences between string and text data types on Rails