ActiveRecord is a Ruby gem that serves up an object relational mapper (ORM) for relational database management systems (RDBMSs). In this post, we'll explore how to use it outside of the Rails ecosystem in the service of fleshing out a simple domain model.
ActiveRecord provides a Ruby interface for creating, reading, updating, and deleting a database and its constituent tables and data. This interface comes without requiring you to rewrite more than a configuration file when switching from SQLite to PostgreSQL. Databases are a useful addition to any Ruby application because they give us a place to save data--Ruby "flushes" data after each execution session.
A lot of people encounter ActiveRecord for the first time when crossing the tracks into Rails territory, but ActiveRecord can be leveraged without the trappings of the entire Rails framework. Any Ruby project that involves a database can benefit from the features ActiveRecord provides!
Let's start by laying out a basic file directory structure for our application. You may have you own ideas and preferences--that's more than alright. The structure I'm about to present is a stripped-down version of what you might see in a Rails project. Go to town laying this out with
touch in your directory or start clicking away in your file browser.
app/ » models/ » planet.rb config/ » environment.rb » database.yml db/ » migrate/ » seeds.rb Gemfile Rakefile README.md
It takes a village to raise an application! We'll start by filling in the
Gemfile in our project directory and calling out the gems we want and need.
# where Bundler will look for gems if they're not already installed source 'https://rubygems.org' # Ruby's quintessential ORM gem 'activerecord' # generates fake data, mostly strings gem 'faker' # allows us to create and run tasks from the command line gem 'rake' # enables us to import a whole folder's worth of code into a file gem 'require_all' # gives us useful Rake tasks for managing a database gem 'sinatra-activerecord' # provides us with a primitive Ruby-SQLite3 database gem 'sqlite3'
If you don't have Bundler installed, now's a good time to hop on the wagon.
gem install bundler ought to do the trick in your terminal.
With Bundler installed, installing our project's gems is as easy as
bundle install (or
bundle, if you're feeling lazy). Bundler scans the gem file for our projects dependencies and downloads and installs whatever is not already on our system. Bundler makes it quick and easy to get any system up to speed with a Ruby application.
Let's take a little trip into the
config/ directory. In
environment.rb we will centralize importing gems and local Ruby files so that we need only require
environment.rb in our run files to get things done.
require 'bundler' Bundler.require require_all 'app'
That wasn't so hard, was it?
require 'bundler' makes it so that we can execute the following line
Bundler.require and essentially
require everything in the
require_all 'app' requires all local
.rb files in the
Now comes the weird part. In
database.yml, copy-paste the following code [source]:
# SQLite version 3.x # gem install sqlite3-ruby (not necessary on OS X Leopard) development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000
Here we specify what kind of RDBMS we are interfacing with, the name and location of the database file (if it doesn't exist it will be generated), then maximum number of open connections (pools) to the database, and the time limit for query operations.
sinatra-activerecord will refer to this file by default to configure the connection between ActiveRecord and the SQLite database.
Rake is a Ruby gem that allows us to run tasks, or snippets of Ruby code, from the command line. It also gives us the ability to tidily define these within a file known as the
Rakefile. In our
Rakefile, let's write the following:
# imports everything (gems and local files) specified in environment.rb require_relative 'config/environment' # gives us an arsenal of rake tasks for managing our database require 'sinatra/activerecord/rake' # describes the task desc 'starts a console' # establishes the name of the rake option: console task :console do # turns on logging of SQL queries while in the task ActiveRecord::Base.logger = Logger.new(STDOUT) # starts a Ruby REPL session Pry.start end
Now, by executing
rake console in our terminal while in our project directory, we will be able to start a REPL session and experiment with methods to our hearts content, all with access to the contents of our project and without the fuss and muss of managing a run file.
While ActiveRecord and Rake are separately invaluable gems, their power and usefulness is compounded with the inclusion of
sinatra-activerecord. Right now, while in your project directory in your terminal, run
rake -T in the command line to see your available tasks. See all the ones with the
db: prefix? Sinatra gave us those.
One of the advantages of using ActiveRecord is that it is an object relational mapper. It gives us the ability to map the structure of our database to the structure of our codebase. Here's a quick refresher on the Active Record pattern of ORMs (for which the ActiveRecord gem is named) with the database element on the left and equivalent codebase element on the right:
- Database = Domain
- Table = Model
- Column = Attribute
- Row = Instance
With ActiveRecord, making our model participate in this scheme is as simple as dirt (or at least inheritance).
app/models/planet.rb, park the following lines:
class Planet < ActiveRecord::Base end
class Planet < ActiveRecord::Base is our golden goose, as it endows our Planet class (aka our Planet model) with all of the rights and privileges and methods of ActiveRecord therein.
I've stuck a class method in there to print all instances of our Planet class with a little flavor. Yet we've no instances to speak of! Let's fix that.
ActiveRecord will look specifically for a
db/ folder at the top of your project directory, along with a
migrate/ subdirectory. ActiveRecord builds out a database with Ruby files called migrations, which specify the action to take on the database and the schema being added or altered.
sinatra-activerecord, we have access to a Rake task that will generate a migration file for us. Run
rake db:create_migration NAME=create_planets in your terminal.
You should now have a file that looks like
TIMESTAMP_create_planets.rb. Inside it, input the following:
class CreatePlanets < ActiveRecord::Migration[6.0] def change create_table :planets do |t| t.string :name t.boolean :inhabitable t.timestamps end end end
Here is the table that corresponds to our model, along with specifications for the tables columns (aka the model's attributes). Note that the table name is plural while the model name was singular (see: ActiveRecord Naming Conventions).
To create this table in our database, we need only lean on another rake task sung by Sinatra:
rake db:migrate. If the migration was successful, we will have a
schema.rb file in the
db/ directory. Check it! Don't edit it ever! It's created and updated with each migration. It's contents should reflect the contents of our migration.
We've come a long way. It's high time we paused to test the makings of our project.
It is critical we test the functionality of our model and database before further fleshing out our project. While we could always require the formal
rspec gem and write formal tests, we aren't going to do that during this foray. Instead, we'll populate our database with seed data and poke at it while in our
seeds.rb file, we are encouraged to write ActiveRecord methods that populate our database (hence the name "seeds"). We will do just that:
boolean_array = [true, false] 5.times do Planet.create( name: Faker::Movies::StarWars.planet, inhabitable: boolean_array.sample ) end
These 7 lines generate 5 planets with random names and inhabitability booleans. To seed our database with this data, we simply run
rake db:seed in our terminal while in the project directory. We know our seeding was successful if we get no immediate feedback.
With our database seeded, let's see if we can do some damage. Pop into the rake console by executing
rake console in your terminal. This command should start a Pry session.
Let's flex some ActiveRecord methods. Our Planet model inherited from
ActiveRecord::Base, so it should have gotten something for it's trouble! Here are a few things you can try:
Planet.create(name: STRING, inhabitable: BOOLEAN)creates a new instance of the Planet class with the specified parameters and saves it in the database
Planet.allreturns all instances of Planet
Planet.find(2)returns the Planet instance with
id == 2
planet.update(name: STRING)updates the instance of Planet (planet) with the given attribute-value pair
Planet.all.last.deletedeletes the last instance of Planet in the database
Full CRUD, just like that! These are but a few of the methods ActiveRecord provides for you. Read the docs to get the full scoop.
Once you grasp domain modeling, CRUD, and the Active Record pattern, ActiveRecord is as powerful as it is intuitive. We've said nothing about relationships between models or validating model attributes.
ActiveRecord empowers us to create and manage RDBMS in sweet, sweet Ruby. We don't need to fret about what flavor of SQL we're using or translating a sorted polymorphic has-many select into a specific query. We can focus on adhering to a rock-solid object-oriented programming pattern to build a readable and maintainable application.
That isn't to say we should simply skip over SQL (in fact, if you don't know SQL, take an hour or two the next chance you get and work through SQLBolt). A basic understanding SQL and RDBMS will help you understand what's going on under the hood of ActiveRecord so you can fix things when they inevitably break down.
When you build out your next (or current) project, think of it in terms of the data flowing through it and how that data can be encapsulated and described. Draw out a domain model and think of the models that make it up and the relationships between them. Think about the attributes that describe those models. Then make your models, write your migrations, seed your database, and play!