We're going to work on changing a Sinatra application from using hard coded hashes as it's data source to using a PostgreSQL database with Active Record.
Active Record provides an interface for interacting with your database and converting your results directly into Ruby objects.
# Clone down the contact manager repo
git clone git@github.com:LaunchAcademy/contact_manager.git
# Move into the contact manager repo directory
cd contact_manager
# Install all the gems
bundle install
# Start the app server
shotgun app.rb
# Open the app in your browser
open http://localhost:9393
In order to get Active Record working with Sinatra, we're going to use the sinatra-activerecord gem.
Add the following gems to your Gemfile:
gem 'sinatra-activerecord'
gem 'pg'
gem 'rake'
Don't forget to run bundle install
and restart your server.
Now require sinatra/activerecord
in your Sinatra application file:
require "sinatra/activerecord"
Configure your database by creating a config/database.yml
with the following:
# Configure the database used when in the development environment
development:
adapter: postgresql
encoding: unicode
database: <YOUR_APP_NAME>_development
pool: 5
username:
password:
# Configure the database used when in the test environment
test:
adapter: postgresql
encoding: unicode
database: <YOUR_APP_NAME>_test
pool: 5
username:
password:
# Configure the database used when in the production environment
production:
adapter: postgresql
encoding: unicode
database: <YOUR_APP_NAME>_production
pool: 5
username:
password:
sintatra-activerecord
also gives us a bunch of rake
tasks that allow us to
create our database, add/drop tables, etc. Rake tasks are basically just ruby
scripts that you can run from the command line.
Require the rake tasks by creating a Rakefile
with the following:
require "sinatra/activerecord/rake"
require "./app"
We can finish our database setup by using a rake task to actually create our database.
Enter the following command in your terminal:
rake db:create
When we run this rake task, Active Record will look at the
config/database.yml
file to determine what kind of database want to use, how
to talk to it, and how to create it.
That's it! If you didn't see any errors, you've got your Sinatra app ready to store data in a database the same way that Rails does.
Protip: You can see all of the rake tasks that are available to you by using
the rake -T
command on the command line.
Active Record uses migrations to create tables in the database. Migrations allow us to create our tables using Ruby code.
We can create a migration file by using the db:create_migration
rake task given to us by
sinatra-activerecord
.
Create a migration that will create a "contacts" table with the following command in your terminal:
rake db:create_migration NAME=create_contacts
By running the previous rake task we've generated a db/migrate
directory for
our app. Inside of that directory you should see a file named something like
20140314134004_create_contacts.rb
. Your actual file name will be slightly
different because the first part (the numbers) is actually a timestamp,
indicating when you created the file, which is used to make sure that our
migrations run in the order that we create them.
Modify your ...create_contacts.rb
migration file so that it looks like this:
class CreateContacts < ActiveRecord::Migration
def change
# Create the contacts table with the following
create_table :contacts do |table|
# A column first_name of type string
table.string :first_name
# A column last_name of type string
table.string :last_name
# A column phone_number of type string
table.string :phone_number
end
end
end
What does it mean to "run" your migrations? Your migration files define how to create/remove/modify the tables and table columns in your database. Simply creating the migration file doesn't actually perform any of those actions, they're just instructions for how to do so once we decide that we want to.
When we're done writing our migration file, we can then "run" our migration by using the following rake task:
rake db:migrate
You should see output similar to this:
== CreateContacts: migrating =================================================
-- create_table(:contacts)
-> 0.0095s
== CreateContacts: migrated (0.0096s) ========================================
When we run this rake task, Active Record runs the ruby code in any of the
migration files that are prefixed with a timestamp later than the version timestamp
in your db/schema.rb
. This is important because we don't want to rerun any
migrations that we have already run in the past.
Before moving on, it's good practice to make sure that your migration can be
rolled back. Rolling back a migration is basically like hitting the undo button.
Active Record is pretty good at guessing what to do in order to undo our
create_table
because the opposite is to simply drop the table with
drop_table
. You will eventually run into other situations where you will need to
manually define what to do for both migrating "up" and rolling back "down" but
we're not going to cover that here.
Make sure that your migration can be rolled back, and then migrate "up" again by running the following commands in the terminal:
# Roll the database back to where it was before the last migration
rake db:rollback
# Run our migration to create the contacts table again
rake db:migrate
Protip: Eventually you will want to create an alias that does both of these commands for you at once. I would recommend against doing that for a while until you have both steps firmly planted in your mind.
The first step is to alter our class definition so that our class is
inheriting
from ActiveRecord::Base
:
class Contact < ActiveRecord::Base
# ...omitted
end
Inheriting from ActiveRecord::Base
, gives the Contact
class all of the
methods that are defined in the ActiveRecord::Base
class.
The next step is to remove our attr_reader
statements and our #initialize
method. This is some of the "rails magic" kind of stuff that you hear people
talking about. We want to remove that stuff is that because the
ActiveRecord::Base
class already defines an #initialize
method for us. Our
initialize method was expecting a hash with all of the model's attributes as a
parameter, which happens to be the same way that ActiveRecord::Base#initialize
works.
Your models/contact.rb
should now look like this:
class Contact < ActiveRecord::Base
def name
[first_name, last_name].join(' ')
end
end
Since ActiveRecord::Base#initialize
works the same way that our
Contact#initialize
method was working, our app should still be working.
Before continuing, make sure that the app is still working by opening the index page in your browser.
It's great that our app is still working but it's also still creating our
Contact
objects by reading their information from the hash. It would be better
if we were reading the information for our contacts out of the database.
In app.rb
, remove the before
block and modify the index action so that it
uses Contact.all
to retrieve all of the contacts form the database:
get '/' do
@contacts = Contact.all
erb :index
end
Here we're using the .all
class method that Active Record gives us. .all
is
one of the many methods that make up the Active Record Query Interface.
Contact.all
will produce a SQL query that looks like this:
SELECT "contacts".* FROM "contacts"
Active Record will take the results of this query and, based on naming
conventions and our table being named contacts
, create an instance of
Contact
for each row that is returned. It will also define accessor methods for
each of the columns, based on the column name.
Open the index page in your browser. What's wrong?
You're right! Our contacts table in the database is empty.
Create a db/seeds.rb
file with the following:
contact_attributes = [
{ first_name: 'Eric', last_name: 'Kelly', phone_number: '1234567890' },
{ first_name: 'Adam', last_name: 'Sheehan', phone_number: '1234567890' },
{ first_name: 'Dan', last_name: 'Pickett', phone_number: '1234567890' },
{ first_name: 'Evan', last_name: 'Charles', phone_number: '1234567890' },
{ first_name: 'Faizaan', last_name: 'Shamsi', phone_number: '1234567890' },
{ first_name: 'Helen', last_name: 'Hood', phone_number: '1234567890' },
{ first_name: 'Corinne', last_name: 'Babel', phone_number: '1234567890' }
]
contact_attributes.each do |attributes|
contact = Contact.new(attributes)
contact.save
end
The db/seeds.rb
file is another one of those kind of magic parts of Active
Record. Remember those rake tasks that Active Record is giving us, that allow us
to run bits of Ruby code? rake db:seed
is one of them. This rake task will
look for a db/seed.rb
file and run the code that we write inside of it.
The db/seeds.rb
file is a good place to write code that populates your
database with some records. In our case, we're using that same hash of data to
create a new instance of Contact
, followed by using Contact#save
to save all
of the contacts attributes into the database. Save is another one of the methods
that we get for free from Active Record.
We can even simplify our seed code by using Contact.create
instead of
Contact.new
:
contact_attributes.each do |attributes|
Contact.create(attributes)
end
Contact.create
will both create a new instance of Contact
and save it to the
database.
To put our seed code into action, run rake db:seed
. This will run all of the
code we wrote in db/seeds.rb
.
You should now be able to view all of the contacts again on the index page.
You might have noticed that all of our tests are failing.
Inside spec/spec_helper.rb
, which is required by all of our test files, we're
setting the environment to test with set :environment, :test
. Because our
tests are running in the test environment, Active Record is being connected to
a different database than the one that we use in the development environment.
If you remember back to when we created our db/database.yml
, we created
separate instructions for each environment. Go back and look at that file again
if you need a refresher.
Earlier when we ran rake db:migrate
, we were only running the migrations on
our development database. This means that our test database doesn't have the
contacts table that is created from our migration.
We can use another one of the default Active Record rake tasks to prepare our test database so that it matches our development database:
rake db:test:prepare
All the tests should now be back to passing!
Hint: You will want to run this rake task after every time that you run rake db:migrate
.