Active Record Migration
The following material is intended for readers with a basic understanding of Ruby, SQL(ite), and using a Framework — in this case Active Record (AR)is the framework we will use to demonstrate the database build process.
The Rails Guide on Migrations clarifies the definition of database migrations:
“Migrations are a convenient way to alter your database schema over time in a consistent way. They use a Ruby DSL so that you don’t have to write SQL by hand, allowing your schema and changes to be database independent.
You can think of each migration as being a new ‘version’ of the database. A schema starts off with nothing in it, and each migration modifies it to add or remove tables, columns, or entries. Active Record knows how to update your schema along this timeline, bringing it from whatever point it is in the history to the latest version. Active Record will also update your
db/schema.rb
file to match the up-to-date structure of your database.”
To expand on that a bit: AR will act as a local (to the app’s directory) version control system. When used correctly - the conventions for Ruby code take precedence in building out the database. Following the naming conventions for tables, classes, and their relations will save huge amounts of time compared to hard-coding SQL inside our files. AR, when used properly, allows many of our building database steps to feel more automated. This meta-programming is where we save our time in building an app.
Connecting the database
With AR convention is key. To give AR the required details w/ regard to connecting a database we use the following dir/file:
config/database.yml
For testing purposes SQLite will handle the job of ensuring proper function for an app in the early development phase. The database.yml code will look like this
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000development:
<<: *default
database: db/development.sqlite3test:
<<: *default
database: db/test.sqlite3production:
<<: *default
database: db/production.sqlite3
Common Rake tasks come with the sinatra-activerecord gem. You can view which of the tasks are available in the console by typing:
rake -T
These tasks are available through
require 'sinatra/activerecord/rake
The config/environment.rb file also fulfills the Rakefile requirements. Code will resemble something like:
# config/environment.rb #environment variable included used by sinatra-activerecord #connected to development-level database
ENV["RACK_ENV"] ||= "development"
require 'bundler/setup'
Bundler.require(:default, ENV["RACK_ENV"])
Now the program has access to the gems from Gemfile. We’re able see what Rake tasks can help with in developing a program in sinatra-activerecord.
Using Rake Tasks to Create Migrations
The following command will setup a table with the assigned name:
$ bundle exec rake db:create_migration NAME=create_developers
This will create the appropriate ruby file in db/migrations. It is important to leave the file names unchanged. The time-stamp which is auto-generated helps you to track the evolution of your development and will also tell sinatra-activerecord which order to run the migrations. Opening the file you’ll find that the Rake task comes with starter code:
# db/migrate/20210716095220_create_developers.rbclass CreateDevelopers < ActiveRecord::Migration[6.1] def change
endend
The CreateDevelopers class is inheriting from AR’s migrations. We’ll go ahead and fill out the method in this class so that a developers table is created. Notice the table names are plural when built using ActiveRecord. The CreateDevelopers table may look something like this:
# db/migrate/20210716095220_create_developers.rb
class CreateDevelopers < ActiveRecord::Migration[6.1]
def change
create_table :developers do |t|
t.string :name
t.string :prime_tech
t.integer :age
t.string :hometown
# the id column is auto-generated for tables!
end
end
end
Notice the familiar ruby code inside create table. This block is setting up the appropriate data type with the passed in ‘t’ variable which has a corresponding column name. I was taught to consider the passed in ‘t’ as representing data-type for the column. With my limited experience I have yet to see this variable name changed to anything else.
Run Migration RUN!
Now we have a migration ready to run. In the console and the appropriate directory we execute the following command and see it’s standard output indicating a successful, error-free migration:
bundle exec rake db:migrate== 20210716095220 CreateDevelopers: migrating ====================================
-- create_table(:developers)
-> 0.0008s
== 20210716095220 CreateDevelopers: migrated (0.0009s) ===========================
Based on the configuration in database.yml we will notice that AR creates a new database file if there isn’t one present already. (NOTE: The SQL code to make all this happen is automated by AR since our environment is configured correctly and the migrations have followed the sensible conventions of plural table names and singular class name.) So we’ll get a very handy db/schema.rb file to ensure our tables/database are set up to our liking.
ActiveRecord::Schema.define(version: 2021_07_16_095220) do
create_table "developers", force: :cascade do |t|
t.string "name"
t.string "prime_tech"
t.integer "age"
t.string "hometown"
end
end
Above: the version number matches the timestamp to the migration file, and we see the table defined! Still need further confirmation this migration ran? Rake has a task for you
$ bundle exec rake db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20210716095220 Create developers
Status = up so we’re good to go on testing the Database.
Using Ruby Methods to Interact with DB
Inside the app/models directory is where we find our Ruby class for the given table. It is inheriting from ActiveRecord’s base commands. Again take notice we’re using a singular class name to correspond with the plural table name. This is part of the aforementioned conventions for satisfying AR’s expectations of us as developers. Failure to comply with these conventions will cause AR to not work and no doubt confuse your teammates.
# app/models/developer.rb
class Developer < ActiveRecord::Base
end
Simple methods can be called on this class from within your Rake console by running
$ bundle exec rake console
Ensure the class exists by typing in “Developer” into the console which will return the following:
# => Developer (call 'Developer.connection' to establish a connection)
Another quick example of a method to use: Developer.column_names will return the Developers table’s column names.
From inside the rake console we can create a new Developer on the fly:
thanos = Developer.new(name: 'Thanos')thanos.hometown = Titanthanos.save
Return values from these commands will verify the information you’re entering and return ‘nil’ for the unassigned attributes. We want to make sure the variable we create in the console will also find it’s home in the DB so we call .save on the newly created developer. You can simultaneously instantiate and save to database with the .create method and save some time typing.
A wide cross-section of CRUD methods are available to use in the database. Be sure to check ’em out.
Migrations Reformat Existing Tables
With a migration that’s already up and running we can not add new code. Rerunning a migration won’t work either. None the less we may be required to expand the developers table.
A new migration will do the trick! We’re gonna set that up very similar to the previous create_developers migration format
#inside terminal
$ rake db:create_migration NAME=add_favorite_hobby_to_developers#inside the file created by new migration
class AddFavoriteHobbyToDevelopers < ActiveRecord::Migration[6.1]
def change
add_column :developers, :favorite_hobby, :string
end
end
Reminder: AR will execute your migrations in alpha-numerical order. This is where having the timestamps ensures expected outcome from our development flow. Use the same rake command as earlier to run the migration. Personally I follow this up with a good look at the schema to ensure the additions were successful.
ActiveRecord::Schema.define(version: 2021_07_16_101748) do
create_table "developers", force: :cascade do |t|
t.string "name"
t.string "prime_tech"
t.integer "age"
t.string "hometown"
t.string "favorite_hobby"
end
end
Practicing these steps will allow your developer-mind to reinforce what’s happening inside Ruby, Active-Record, and Sinatra. Having this under-the-hood-level understanding makes us better at debugging our own code as well as others’ code. As a student I have found that these more involved approaches to full-stack development help create clearer understanding when moving onto more popular technologies. In this case I’m referring to what comes next in my program: Ruby on Rails.