Greymeister.net

Meet Our New DBA: ActiveRecord Using JRuby and Gradle

GitHub link: activeRecordMigrate

On my current project, in true Agile fashion, we had avoided using a database because we didn’t need one. Eventually, we had requirements for storing data that was coming from an external source, and at this point we had to start making decisions on how we would move forward. We had kept our project “lean” and the biggest concern I had was not introducing a build step that would include installing MySQL or something else that would drastically depart from our build environment.

I had been reading off and on the book Rails for Java Developers by Stuart Halloway and Justin Gehtland.  I recommend it for all developers, not for either a Java or Rails reference, but for interesting comparative analysis on web development technologies.  One of the things mentioned in that book is using ActiveRecord for schema versioning even in applications not using Rails. After playing around with ActiveRecord in their sample code for awhile, I thought it would clearly provide two big wins:

  1. No writing a ton of SQL!
  2. Support any DB that AR supports.

#2 was especially important in our case as we were still not certain what RDBMS we would end up using, and that decision may not be up to us in the end anyway. This is also relevant for #1 because there was going to be DDL rework later down the road which would not provide much value. So plug in ActiveRecord and “Hasta Lasagna, don’t get any on ya” right? There was still one major hurdle in all of this though: I work in a Java shop. However, we already use Groovy and have started to move from Maven to Gradle for our builds. Therefore, if I could have ActiveRecord work in that ecosystem, it would still be viable. Naturally, the choice to do this was JRuby.

I was able to get started using the information in Robert Fischer’s blog post on running Cucumber and JRuby from Gradle. However, I encountered some problems early on with getting the gems necessary to run ActiveRecord. Basically, I wanted to make sure that the gems would always be installed in the same place, and also not to require developers to install JRuby to use the build. I solved that problem with two configuration options. First, I made a gemrc.yml file in the project that set the gem options to always be the same. Secondly, I had to make sure that the gems were going to be on the classpath so that they would be available. I found out how to do this based on Nick Sieger’s blog post on putting Gems-in-a-jar. At that point, everything needed to run ActiveRecord from Gradle was available. Here are the relevant sections of the build.gradle file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import org.jruby.Main as JRuby
// Location to put the gems
gemsDir = './build/gems'

// List the gems you want here ..
rubyGems = ['jruby-openssl','activerecord','activerecord-jdbcsqlite3-adapter',
            'rake']

// Software dependency versions
jrubyVersion = '1.5.6'

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'org.jruby.embed:jruby-embed:0.1.2' // See Notes
  }
}

dependencies {
  runtime "org.jruby:jruby-complete:${jrubyVersion}"
}

repositories {
    mavenCentral()
}

processResources.dependsOn 'gems'

sourceSets {
  main {
    resources {
      srcDir "$gemsDir"
    }
  }
}

task gems << {
  def configSettings = '--config-file gemrc.yml'
  rubyGems.each { gemName ->
    if(!hasGem(gemName))
      runJRuby("gem install $gemName $configSettings")
  }
}

def hasGem(gemName) {
  new File("${gemsDir}/gems").list().find {it.startsWith(gemName) } != null
}

// Helper class to set the database property on the project
// By default, it will be development
task(envHelper, dependsOn:classes) {
  if(!project.hasProperty('database'))
    project.setProperty('database', 'development')
}

// Migrate the db
task(migrate, dependsOn:envHelper, type: JavaExec) {
  main = 'org.jruby.Main'
  classpath = sourceSets.main.runtimeClasspath
  args = ['-S', 'rake', 'migrate', "RAILS_ENV=${project.getProperty('database')}"]
}

def runJRuby(cmdArg) {
  def cmd = "-S $cmdArg"
  println "Running JRuby: $cmd"
  Thread.currentThread().setContextClassLoader(JRuby.class.classLoader)
  JRuby.main("$cmd".split())
}

The key parts here are gems, migrate, and runJRuby. With gems as part of the resources, it will be on the runtime classpath, meaning that the gems will be visible when I call org.jruby.Main to run the commands I’m passing it. To make sure that the gems path is already set correctly and not require any configuration by the developers, here is the gemrc.yml file that exists in the source directory:

1
2
3
4
gem: --no-rdoc --no-ri
gemhome: ./build/gems
gempath:
  - ./build/gems

This makes sure that when installing gems, it always puts them into the build directory, so that all the rest of the tasks will see them on the runtime classpath. My Rakefile is fairly simple, but I’m including it for informative purposes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require 'rubygems'
require 'active_record'
require 'yaml'
require 'logger'

task :default => :migrate

desc "Set up the environment and connect to the database specified by RAILS_ENV"
task :environment do
  @logger = Logger.new STDOUT
  RAILS_ENV = (ENV['RAILS_ENV'] ||= 'development')
  dbconfig = YAML::load(File.open('database.yml'))[RAILS_ENV]
  ActiveRecord::Base.establish_connection(dbconfig)

  # Enable diagnostic logging to help debugging.
  ActiveRecord::Base.logger = @logger
  ActiveRecord::Base.set_primary_key "Id"
end

desc "Migrate the database through scripts in 'migrate'. Target specific version with VERSION=x"
task :migrate => :environment do
  ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
end

desc "Export the schema/DDL to an output file"
task :schema_export => :environment do
  file = File.open('db/schema.rb', 'w')
  ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end

Next, create your database.yml file containing your environments:

1
2
3
4
5
# Dev DB
development:
  adapter: jdbcsqlite3
  database: db/development.sqlite3
  timeout: 5000

Naturally, the process is to run the Gradle script with “migrate” and it will automatically run any migrate scripts you have under db/migrate. You can create a simple migration script to test out the configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreatePerson < ActiveRecord::Migration
  def self.up
    create_table :person, {:id => true} do |inst|
      inst.column :name, :string, :limit => 100, :null => false
      inst.column :url, :string, :null => false
      inst.column :lock_version, :integer, { :default => 0 }
    end
  end

  def self.down
    drop_table :person
  end
end

Running this build from the command line you should see something like the following:

Screen Shot

Obviously, not everyone can start with something as easy as the example above for the 001_migration.rb file. Luckily ActiveRecord solves this problem by being able to dump an existing schema out to a Ruby file that can be your “version 0” migration. I have the task in my Rakefile, adding the Gradle task is pretty trivial but I left it out to save space.

That’s about all there is to it. Now I have project that any developer using Gradle can create a database with. To support different databases we just need to add adapter gems and the appropriate configuration in the database.yml file. Also, did I mention it is much more fun to write Ruby than SQL?

Notes: I’m not sure why, but I had errors using JRuby-1.5.6’s org.jruby.Main class to execute the gems task. I ended up sticking with the old jruby-embed which was in Fischer’s original blog post and that works fine, but I would like to know what the deal is with that.

My sample project is now available on GitHub: activeRecordMigrate