3 min read

ActiveRecord without Rails

I needed a quick and simple database to use with a plain old ruby project, taking ActiveRecord on it's own is pretty easy
ActiveRecord without Rails
Photo by Immo Wegmann / Unsplash

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.