Revibalog

home

Simple Active Record Scratch Pad

03 Oct 2013

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.



comments powered by Disqus