Using Rails Fixtures to Seed a Database

It’s no mystery that I’ve grown to love Rails fixtures. And I tend to mostly use relational databases in my applications, specifically PostgreSQL.

Most applications have ancillary data that’s required to support the main function of the application — think drop-downs with states for shipping or credit card type.

This data is almost always never interesting, but completely necessary for the application to work as expected. So when it comes to time send your little baby to production, only to find your users can’t pay because they can’t pick their credit card type, your world comes crashing down.

If you have those credit card types in fixtures from the start, loading them in to your development of production database is just a rake task away.

The Problem

Let’s assume our application requires us have a list of supported credit card types, and the user is required to pick from the list to pay for the awesome stuff we sell. A sample fixture might look like:

visa:
  name: Visa

mastercard:
  name: Mastercard

amex:
  name: American Express

This is a somewhat trivial example because the name matches what one might expect in a potential transaction record if we had a credit_card_type field or something similar if we denormalized.

Perhaps we have a field credit_card_type_id in a transactions table that references the foreign key of the related CreditCardType record.

So how do we get these records in to our development and production databases?

The Solution

Fortunately, Rails has our backs. The following rake test is available from a default Rails application:

$ bin/rake -T
...
rake db:fixtures:load # Load fixtures into the current environment's database

The db:fixtures:load task is an interesting start, but quickly we realize it might be a little heavy-handed. If this application has users, we probably wouldn’t want to copy them to production. They might, however, be a great starting pointing for development.

So how do we handle getting trivial model data in to production for only specific models?

It turns out that we can specify ONLY the models we want to load by using the FIXTURES environment variable:

rake db:fixtures:load FIXTURES=credit_card_types

Note: The name of the fixture file (usually the database table name) should be used as the value for FIXTURES, not the model name.

With that single command, any environment we specify will immediately get the data for our 3 credit card types.

A word of warning, if we run this command multiple times, it will seed the table multiple times. It’s not idempotent.

Additionally, if we wanted to load more than just a single fixture, we can specify the names of the files separated by commas:

rake db:fixtures:load FIXTURES=credit_card_types,states,cities

Let’s take a quick look at how Rails implements this functionality, specifically the determination of single models:

fixtures_dir = if ENV['FIXTURES_DIR']
                 File.join base_dir, ENV['FIXTURES_DIR']
               else
                 base_dir
               end

fixture_files = if ENV['FIXTURES']
                  ENV['FIXTURES'].split(',')
                else
                  # The use of String#[] here is to support namespaced fixtures
                  Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }
                end

ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)

If the FIXTURES variable is present, code teases appart the model names and looks in the fixtures directory and loads the YAML fixture file for that table name.

An interesting side note, it’s possible to specify alternate directories for fixture using the FIXTURES_DIR variable. I personally haven taken advantage of this, but could be useful if you want to keep specific fixture files for production that might be different than those that reside in test/fixtures/*.

I wouldn’t suggesting using this approach for anything that needs to reference other foreign keys. When you’re transferring to a different database, foreign keys will not match and your application will likely not work as expected.

Summary

This approach has saved me quite a bit of time in my last few applications. Build it once, use it everywhere. As mentioned above, using this approach to seed database records with a foreign key should be avoided.

Most applications have a number of tasks needed for a developer to get up and running. Combining fixture data with additional seed data placed in db/seeds.rb can give you the best of both worlds, while still ensuring you have robust data to test against.