UPDATE 2008/10/17: See Part 2 for transation support, including the ability to run multiple tests (which the code below fails to do because of missing transaction support).

I was bored this evening and wanted to do something at least semi interesting. I settled on integrating DataMapper with Rails. Seemed like a nice enough thing to do. Searching for datamapper on rails on Google gave me a link to DataMapper 0.9 avec Rails (Google translation). Nicolas’ article was very interesting, but it’s focus is just on the basics. I decided to tackle migrations and testing.

Unit testing DataMapper models from within Rails

Let’s start by generating a blank Rails application:

1 $ rails dm_on_rails

Then, we’ll remove as much dependency on ActiveRecord as we can. From config/environment.rb, uncomment the config.frameworks line and edit it to the following:

1 # Skip frameworks you're not going to use. To use Rails without a database
2 # you must remove the Active Record framework.
3 config.frameworks -= [ :active_record ]

Rails 2.1 installs some new defaults in config/initializers/new_rails_defaults.rb. Remove the ones that reference ActiveRecord.

Then we need to load the DataMapper gem. Do it by editing config/environment.rb and loading gems:

1 # I use SQLite3 here, but if you need/want MySQL, replace sqlite3 with mysql
2 config.gem "do_sqlite3", :version => "0.9.6"
3 config.gem "dm-core", :version => "0.9.6"

Make sure your gems are up to date by running rake gems:install. I had problems with older versions of dm-core polluting my system. You might want to remove those if you get dependency issues.

Then, we need to configure config/database.yml. Completely replace the data in there with this:

 1 development: &defaults
 2   :adapter: sqlite3
 3   :database: db/development.sqlite3
 5 test:
 6   <<: *defaults
 7   :database: db/test.sqlite3
 9 production:
10   <<: *defaults
11   :database: db/production.sqlite3

Then, we need to load this YAML file to configure DataMapper. Create config/initializers/datamapper.rb:

1 require "dm-core"
2 hash = YAML.load(File.new(Rails.root + "/config/database.yml"))
3 DataMapper.setup(:default, hash[Rails.env])

Next, let’s generate a model to play with.

1 $ script/generate model article title:string body:text
2       exists  app/models/
3       exists  test/unit/
4       exists  test/fixtures/
5       create  app/models/article.rb
6       create  test/unit/article_test.rb
7       create  test/fixtures/articles.yml
8 uninitialized constant Rails::Generator::GeneratedAttribute::ActiveRecord

Seems we can’t use script/generate model. But the the important files have been created: the fixtures, test and model.

Running rake test:recent gives us another error:

 1 $ rake test:recent
 2 (in /Users/francois/Documents/work/dm_on_rails)
 3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb" "test/unit/article_test.rb" 
 4 /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:414:in `to_constant_name': Anonymous modules have no name to be referenced by (ArgumentError)
 5   from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:226:in `qualified_name_for'
 6   from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:491:in `const_missing'
 7   from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:869:in `require_fixture_classes'
 8   from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:867:in `each'
 9   from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:867:in `require_fixture_classes'
10   from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:850:in `fixtures'
11   from ./test/test_helper.rb:35
12   from ./test/unit/article_test.rb:1:in `require'
13   from ./test/unit/article_test.rb:1
14   from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5:in `load'
15   from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5
16   from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5:in `each'
17   from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5
18 rake aborted!
19 Command failed with status (1): [/System/Library/Frameworks/Ruby.framework/...]
21 (See full trace by running task with --trace)

This is caused by the default fixtures :all line in test/test_helper.rb. In fact, all the fixtures code is highly dependent on ActiveRecord. So, we won’t use Rails fixtures with DataMapper. Comment out the fixtures :all line, as well as the lines that specify transactional fixtures options.

Next up, the database isn’t created. Ooops! Let’s use a nice DataMapper feature, auto migrations. Append to the end of test/test_helper.rb:

1 Dir[File.join(Rails.root, "app", "models", "*")].each {|f| require f}
2 DataMapper.auto_migrate!

We begin by loading all models, because at the end of test/test_helper.rb, no model files have been loaded yet. So we have to define the models in memory before DataMapper can auto migrate them.

Edit test/unit/article_test.rb and put the following:

1 class ArticleTest < ActiveSupport::TestCase
2   def test_create
3     article = Article.create("First Post", :body => "This is my first-ever post", :published_at => Time.now.utc)
4     assert !article.new_record?
5   end
6 end

And put real code in app/models/article.rb:

1 class Article
2   include DataMapper::Resource
4   property :id,           Integer, :serial => true
5   property :title,        String
6   property :body,         Text
7   property :published_at, DateTime
8 end

And with that, we get a fully functional DataMapper integration!

 1 $ rake test:units
 2 (in /Users/francois/Documents/work/dm_on_rails)
 3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb" "test/unit/article_test.rb" 
 4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader
 5 Started
 6 .
 7 Finished in 0.005368 seconds.
 9 1 tests, 0 assertions, 0 failures, 0 errors

Running DataMapper migrations from Rails

What about migrations? Well, now we need to enter the world of dm-migrations. Back in config/environment.rb, depend on a new gem:

1 config.gem "dm-migrations", :version => "0.9.6"

Then, create the migration file itself (remember! no script/generate migration for you):

 1 migration 1, :create_articles do
 2   up do
 3     create_table :articles do
 4       column :id, Integer, :serial => true, :nullable? => false
 5       column :title, String
 6       column :body, "TEXT"
 7     end
 8   end
10   down do
11     drop_table :articles
12   end
13 end

Finally, we want a Rake task which will run the migrations. Since the db:migrate namespace is already used, we’ll create a dm:migrate namespace instead. Create lib/tasks/migrations.rake:

1 namespace :dm do
2   task :migrate => :environment do
3     gem "dm-migrations", "=0.9.6"
4     require "migration_runner"
5     Dir[File.join(Rails.root, "db", "migrate", "*")].each {|f| require f}
6     migrate_up!
7   end
8 end

With all that support in place, it’s time to run the migrations:

1 $ rake dm:migrate
2 (in /Users/francois/Documents/work/dm_on_rails)
3  == Performing Up Migration #1: create_articles
5    -> 0.0005s
6  -> 0.0010s

If you wish to see the code in action, you may checkout the GitHub repository dm_on_rails. Each commit corresponds to a step in this article.


Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend


Projects I work on

Projects I worked on