Michael Davis
Posted on January 31, 2023
Follow along with my example repo, which has a solution branch.
So, you wanna be a Ruby on Rails back end engineer, huh?? Then you came to the right place. 👊🙂
As of the posting of this article, I am finishing up my fourth phase of Flatiron School's full-stack software engineering boot camp. Phase 4 was focused on the topic of this article: Ruby on Rails. As an abstraction of Ruby, Rails expedites many of the tasks necessary to set up a Ruby database. What's the best way to do this, you ask? Well, let's find out! But first, let me briefly outline the data examples to best illustrate how our back end is built.
I'm a big music fan, and a full album listener at that, so I'd like to use the example of music fans giving ratings to particular albums. Each music fan can rate many different albums, and each album can be rated by many different music fans. It would make sense to have each music fan to only be able to rate each album once, because otherwise it would get confusing. Lastly, each rating will belong to one music fan and one album, so ratings will act as our join table with a foreign key for each of the other tables named music fans & albums.
So, each of our tables will have these attributes/columns:
- Music Fans - :name
- Albums - :title, :artist, :release_year
- Ratings - :score, :music_fan_id, :album_id
Creating our associations will come into play later on, but now that we know what data we're going to be working with, let's review our Rails resources...
Rails Resources
In a Ruby on Rails application, there are a few essential things to keep an eye on at the highest level of our file hierarchy:
- Gemfile (list of gems to install)
- db/ (database folder)
- config/ (configurations folder)
- app/ (application folder)
If you are in the habit of paying attention to these files and folders, then you won't be caught off guard by any of the basic issues with building out your Rails back end!
We do have a single terminal command that will build out all of the aforementioned resources for us, in the form of:
rails g resource class_name attribute_name:attribute_type
...BUT, it's really important to understand all of the things that this command is doing, and how to properly write the command based on your particular dataset. So, let's start from the top.
Let's Break it Down by Each Resource...
Gemfile
As mentioned above, this is where we list our gems to be installed - these are our Ruby tools that make our life easier when building this code. For our purposes, we want to make sure our gemfile includes:
gem "active_model_serializers", "~> 0.10.12"
...and then to install these gems for use in development and testing, we enter this in our terminal:
bundle install
NOTE: All of your other needed gems should be included if you properly created your rails application with:
rails my_music_app
After creating our app's file structure and installing our gems, we can move on and start building out our data structure.
Database Folder - db/
Now the fun begins. Our database folder is going to contain FOUR important things:
- db/migrate (migrations folder)
- schema.rb (database structure file, based on migrations)
- seeds.rb (seed data, formatted for schema.rb)
- development.sqlite3 (final rendered database file)
Let's break it down...
1. db/migrate - (migrations)
This is the folder is where it all begins. Our migration files set up the data structure for each of our tables, including both the name of the table itself and the labels for each column attribute. Each table will have its own migrations. For our Ratings table (the join table with foreign keys), this is what our initial migration file would look like if we built it manually (but hold off on this for just a minute):
class CreateRatings < ActiveRecord::Migration[6.1]
def change
create_table :ratings do |t|
t.integer :score
t.integer :music_fan_id
t.integer :album_id
end
end
end
2. schema.rb - (database structure)
Our schema file will act as the data structure resulting from the migrations for all of our tables, acting as a filter for our incoming seed data. This file will look similar to our migrations, but will include all the tables. To create our schema file manually (again, hold off on this too unless you want practice), we need to run this in our terminal:
rails db:migrate
Our resultant file should look something like this, including all three of our table migration structures:
ActiveRecord::Schema.define(version: 2023_01_20_164246) do
create_table "albums", force: :cascade do |t|
t.string "title"
t.string "artist"
t.integer "release_year"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "music_fans", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "ratings", force: :cascade do |t|
t.integer "score"
t.integer "music_fan_id", null: false
t.integer "album_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
It's always good to double check your schema file after performing rails db:migrate, just to make sure things are looking accurate. So, now our data structure is in place for us to seed our data tables with. BUT, we can't seed our data without...
3. seeds.rb (our seed data)
This is the file in which we set up the actual data values for our table to be populated with. Without our seeds file, we won't be able to utilize our schema, because it won't have anything to work with. Basically, without a functioning seeds file, our resulting database will be comprised of empty tables. We don't want that, so let's look at what we need to do to build our seed data.
There are many Ruby methods we can use in building our seeds file, but we will keep it simple this time. If we were to seed with 3 instances each of music_fan and album, along with 6 unique rating instances, this is what it might look like:
puts "🎵 Seeding music fans..."
MusicFan.create(name: "Vizmund Cygnus")
MusicFan.create(name: "Cassandra Gemini")
MusicFan.create(name: "Cerpin Taxt")
puts "🎵 Seeding albums..."
Album.create(
title: "DAMN. COLLECTORS EDITION.",
artist: "Kendrick Lamar",
release_year: 2017
)
Album.create(
title: "In Rainbows",
artist: "Radiohead",
release_year: 2007
)
Album.create(
title: "Monsters Are People",
artist: "Chess at Breakfast",
release_year: 2021
)
puts "🎵 Seeding ratings..."
Rating.create(score: 10, music_fan_id: 1, album_id: 2)
Rating.create(score: 9, music_fan_id: 3, album_id: 1)
Rating.create(score: 8, music_fan_id: 2, album_id: 3)
Rating.create(score: 7, music_fan_id: 3, album_id: 3)
Rating.create(score: 6, music_fan_id: 2, album_id: 2)
Rating.create(score: 5, music_fan_id: 1, album_id: 1)
puts "Done seeding!"
_PRO TIP: If you want to get practice using the Faker Gem to automate some of the details of your seeds file, _
As you can see, our seed data is built using pretty basic Ruby object format, with keys corresponding to our table attributes and unique values for those keys in each new creation of an instance. And luckily, we don't need to hold off creating this seeds file, since we will always have to create it manually.
NOTE: Each creation of an instance assigns id based on the order in which they're built, so the first MusicFan (Vizmund Cygnus) will have an id of 1, and the second (Cassandra Gemini) will have an id of 2, and so on...
With our fleshed-out seed data now created, we should be in a position to seed and render our database.
4. db.sqlite3 - (rendered database)
Assuming our seed data and migrations are formatted correctly, we can now run this command in our terminal:
rails db:seed
Once we seed, we should now have a development.sqlite3 file in our db/ folder. Right-click on this and select "Open Database". This will open up your SQLite Explorer in the bottom-left corner of your VS Code window. NOTE: Make sure you've installed SQLite as an extension in VS Code, or this won't work!
If you've done everything correctly up to this point, you should have three tables. For example, your albums table should look like this:
If there are ever any weird issues with the database throughout development, you can always try this command:
rails db:reset
...which will drop the database (make sure you actually want to drop it before you do this!) and re-seed.
Now that we have a rendered database, we can finally move on to USING our data!
Configurations Folder - config/
Our config folder luckily only has one file we need to keep a close eye on: routes.rb
This file is where we can determine what CRUD actions to be allowed for our different controllers, as well as setting up custom routes for things like user authentication. We won't take a super close look at this here, but here's what our basic routes file would look like if we have full CRUD on albums and music fans, but only index, show, create, and destroy (excluding update) on our ratings:
Rails.application.routes.draw do
resources :ratings, only: [:index, :show, :create, :destroy]
resources :music_fans
resources :albums
end
Doing this will ensure that we don't have any unused routes on our back end, which will speed up our rendering process.
Now that we have limited our routes, to enhance efficiency, we can move on to our final resource...
Application Folder - app/
Similar to how I went over the config folder, I won't go into too much detail on our app folder, but I will give an overview of the important resources to keep an eye on, along with why. Here's a brief list:
- models (foundational class models)
- controllers (controllers with route actions)
- serializers (serializers for preparing data output)
app/models - class models
Our models are the foundation for the rest of our app folder, because it's where we determine our associations and our validations. First and foremost, we need to build out our associations. Here's how they will look, with a score validation between 1-10:
rating.rb
class Rating < ApplicationRecord
belongs_to :music_fan
belongs_to :album
validates :score, inclusion: 1..10
end
music_fan.rb
class MusicFan < ApplicationRecord
has_many :ratings, dependent: :destroy
has_many :albums, through: :ratings
end
album.rb
class Album < ApplicationRecord
has_many :ratings, dependent: :destroy
has_many :music_fans, through: :ratings
end
NOTE: Our Rating model will already have the belongs_to relationships if we use rails g resource properly. Stick around to the end to see how that's written.
OTHER NOTE: We include dependent: :destroy in order to engage the destroy route of all ratings associated with a particular music fan or album, when they themselves are destroyed. This makes sense because ratings without music fans or albums shouldn't technically exist and will cause issues with rendering in the front end.
Awesome! Now we can move on to our next application resource.
app/controllers - with route actions
Our controllers will contain the methods that our routes.rb file points to. As far as full CRUD, the default actions are as follows:
- index (READ all)
- show (READ one)
- create (CREATE one)
- update (UPDATE one)
- destroy (DELETE one)
To demonstrate full CRUD here, we need to pick one of the models that has full CRUD. For this purpose, let's go with albums:
class AlbumsController < ApplicationController
def index
albums = Album.all
render json: albums, status: :ok
end
def show
album = Album.find(params[:id])
render json: album, status: :ok
end
def create
album = Album.create!(album_params)
render json: album, status: :created
end
def update
album = Album.find(params[:id])
album.update!(album_params)
render json: album, status: :accepted
end
def delete
album = Album.find(params[:id])
album.destroy
head :no_content
end
private
def album_params
params.permit(:title, :artist, :release_year)
end
end
NOTE: we save ourselves a bit of time by defining album_params in our private methods.
We now have all of our CRUD methods built for albums, so all our routes should work properly for it. We can now move on to the final piece in the puzzle...
app/serializers - for preparing data output
We're almost there! With serializers, we are basically telling our routes how to prepare our data. So let's say that when we fetch a particular album, we want to also show all the music fans that have rated that particular album. To do this, we simply need to add one line to our album_serializer.rb. Here's what the file will look like:
class AlbumSerializer < ActiveModel::Serializer
attributes :id, :title, :artist, :release_year
has_many :music_fans
end
This will ensure that when we do fetch that album, the JSON response will have a music_fans key and a corresponding value of an array nesting all the music fans that have rated the album.
NOTE: We don't need to include the through: :ratings here because we have already established that association in our model.
One cool element to serializers is that you can create your own custom ones if you need to have more specific data outputs for particular CRUD actions. I won't go into that here, but I recommend doing some research on it!
Now that we've established the importance of all our resources, we can tie it all together.
Tying it All Together
Jeez. Rails has a LOT of resources to keep track of, eh?? If we had to populate our back end with all of these resources one at a time, it would take awhile for sure. Luckily, we don't have to!
Generators to the Rescue
Rails has a generator feature that will create all of these files for us. However, there is room for error here, so it's very important to understand all the resources that are created by the generator, and how to check these resources for errors. Let's take a look at what the generator commands for each class will look like in our terminal, and then break them down. First, here is the basic formula, as mentioned at the beginning of this article:
rails g resource class_name attribute_name:attribute_type --no-test-framework
NOTE: Include --no-test-framework if you already have testing files and want to avoid overwriting them.
And now our specific class examples:
Rating
rails g resource rating score:integer music_fan:belongs_to
MusicFan
rails g resource music_fan name
Album
rails g resource album title artist
NOTE: The default attribute_type is string, so unless a particular attribute is something else, such as integer, you don't need to actually include the type in your resource generator.
OTHER NOTE: As far as associations, we can only include belongs_to in our resource generator, and need to manually create our has_many associations later on in our individual models.
As you see upon execution in your terminal, this command creates ALL of the resources we have gone over in this article. Pretty cool! If you did this command the right way, you're well on your way to customizing your back end.
I hope this article was helpful, and as always...
Happy Coding!
Posted on January 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.