Sometimes I prefer experimenting with Active Record in an isolated environment to get a clear picture of a particular behavior or API. Let's build a disposable scratch pad with an in-memory database, query logging and a repl with code reloading. We'll use Rake to manage the workflow.
Create a directory with a Gemfile, Rakefile and our scratch pad Ruby file.
$ mkdir ar-explorer && cd ar-explorer/
$ touch {Gemfile,Rakefile,scratch.rb}
The Gemfile should declare two dependencies, activerecord and sqlite3. Run bundle
to install the gems.
# Gemfile
source 'https://rubygems.org'
gem 'sqlite3'
gem 'activerecord'
The scratch file can now require the dependencies and configure the database connection. We'll also use Ruby's stdlib logger library for logging queries to stdout. For convenience, I've specified a custom log formatter to output only the message parameter.
# scratch.rb
require 'logger'
require 'sqlite3'
require 'active_record'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.formatter = proc { |*_, msg| "#{msg.to_s.strip}\n" }
Now we can declare the database structure with the Active Record schema DSL and define a model class. As an example experiment, we'll trace the order of AR's before_save
, after_save
, before_create
and after_create
callbacks.
# scratch.rb
# ...
ActiveRecord::Schema.define do
create_table :users do |t|
t.string :name
end
end
class User < ActiveRecord::Base
before_save { logger.info :before_save }
after_save { logger.info :after_save }
before_create { logger.info :before_create }
after_create { logger.info :after_create }
end
User.create(name: 'Bill Nye')
Running the file outputs the order of execution for the callbacks with query logging interspersed.
$ ruby scratch.rb
-- create_table(:users)
(0.4ms) CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255))
-> 0.0231s
(0.1ms) begin transaction
before_save
before_create
SQL (0.2ms) INSERT INTO "users" ("name") VALUES (?) [["name", "Bill Nye"]]
after_create
after_save
(0.1ms) commit transaction
We now have the foundation of our scratch pad, but being able to interact with it in a manner similar to rails console
would be very useful. Let's encapsulate that behavior as the default Rake task. Before proceeding, remove or comment out the line User.create(name: 'Bill Nye')
.
# Rakefile
task default: :console
task :console do
require 'irb'
require './scratch'
ARGV.clear # IRB will try to parse ARGV otherwise
IRB.start
end
Now we can run rake console
or rake
from the command line and drop into a console session with our scratch environment loaded.
$ rake
-- create_table(:users)
(0.5ms) CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255))
-> 0.0142s
>>
To wrap up our Active Record scratch pad we'll add a reload!
method like the method from rails console
. This will force the scratch file to be reloaded without needing to restart the IRB session.
# scratch.rb - near the top of the file
def reload!
load __FILE__
end
If you prefer not to run the migrations when loading the file, wrap the schema definition in a method and call it at your convenience. We could extract the schema definition into a separate file, but I prefer the simplicity of having a single scratch file.
def migrate!
ActiveRecord::Schema.define do
create_table :users do |t|
t.string :name
end
end
end
Here is a gist with the complete implementation: https://gist.github.com/invisiblefunnel/6815374.