I needed a quick and simple database to use with a plain old ruby project, taking ActiveRecord on it's own is pretty easy.

I was building a test pack that needed to be able to pick a sample from one of several 100K samples to match an external stub. The stubs published their data in a huge JSON file and I needed to select one randomly based of a variable set of criteria. ActiveRecord to the rescue.

First thing to do, add ActiveRecord to the Gemfile and, since I'm going to use sqlite, I'll add that too. I'll also add rake because I want to use rake tasks to manage bringing the database up / down.

# Gemfile 
gem 'activerecord', '~> 6.0'
gem 'sqlite3', '~> 1.4'
gem 'rake', '~> 13.0'

... and then require it in my code

require 'active_record'

I need to configure ActiveRecord to tell it which database driver to use and where the database is. I could hardcode a hash, but let's have a YAML file to hold that config just in case I change my mind about which database to use later.

# config/database.yml
---
adapter: sqlite3
database: db/identities.db

Now I want to create a Rakefile with tasks for creation, migration etc.

# Rakefile
require "bundler/setup"
Bundler.require
require "json"
require "yaml"

namespace :db do
db_config = YAML::load(File.open("config/database.yml"))

desc "Create the database"
task :create do
ActiveRecord::Base.establish_connection(db_config)
puts "Database created"
end

desc "Migrate the database"
task :migrate => :create do
ActiveRecord::Base.establish_connection(db_config)
ActiveRecord::MigrationContext.new("db/migrate/", ActiveRecord::SchemaMigration).migrate
puts "Database migrated"
end

desc "Drop the database"
task :drop do
File.delete(db_config["database"]) if File.exist?(db_config["database"])
puts "Database deleted"
end

desc "Reset the database"
task reset: [:drop, :create, :migrate]

desc "Create a db/schema.rb file"
task :schema do
ActiveRecord::Base.establish_connection(db_config)
require "active_record/schema_dumper"
filename = "db/schema.rb"
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
puts 'Schema dumped'
end

desc 'Populate the database'
task :seed => :migrate do
ActiveRecord::Base.establish_connection(db_config)
load 'db/seed.rb' if File.exist?('db/seed.rb')
end
end

namespace :g do
desc "Generate migration"
task :migration do
name = ARGV[1] || raise("Specify name: rake g:migration your_migration")
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
path = File.expand_path("../db/migrate/#{timestamp}_#{name}.rb", __FILE__)
migration_class = name.split("_").map(&:capitalize).join

File.open(path, "w") do |file|
file.write <<~EOF
class #{migration_class} < ActiveRecord::Migration[6.0]
def change
end
end
EOF
end

puts "Migration #{path} created"
abort # needed stop other tasks
end
end

Getting the migrations to work was a bit tricky. The way they work has changed quite a bit between major versions of ActiveRecord. The above is good for v6.

This Rakefile expects a db directory, just like rails. Migrations will live in db/migrate and there's a very basic generator included. After a bundle install I can run bundle exec rake g:migration books and a migration template will be autogenerated.

Let's create a table.

# db/migrate/20201022121743_books.rb

class Books < ActiveRecord::Migration[6.0]
def change
create_table :books do |t|
t.string :title
t.string :author
end
end
end

and run a bundle exec db:migrate to create the table.

Now I need some model classes. I put these in a models folder and also create a parent class to follow the Rails pattern.

# models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
# models/book.rb

class Book < ApplicationRecord
end

I make sure my source does a require_relative on the two source files above and I can then use all of the power of ActiveRecord, including using scopes, magic finders etc.