Skip to content
pascalh1011 edited this page Aug 22, 2011 · 7 revisions

A brave new world

Migrant lets you describe your database schema via your models, and get validation and basic mocking all rolled into one. If used correctly, it can serve as documentation of your domain model for future developers (and you of course), and takes a lot of the plumbing out of working with ActiveRecord.

I’ll create a really basic Rails app that uses Migrant here; In a great leap of imagination, I’ve decided it will be a railway management app.

For this example I’m using PostgreSQL but you should be able to use any flavour of database that works with ActiveRecord. If you do in fact find yourself using Postgres, I’d highly recommend you check out https://github.com/tenderlove/texticle, which provides full text searching using a very similar DSL to migrant.

Let’s assume we’ve created a new Rails app, added the required gems (including Migrant), bundle installed, and set up our database.

Let there be models!

Our domain model will be fairly simple. We have a number of trains that have routes (from city A to B), and a scheduled service that runs every so often on a given route (belongs to a train and a route).

To create models, you can either add them yourself into app/models, or use the model generator as follows:

rails g migrant:model train

This gets us an empty model as follows:

class Train < ActiveRecord::Base
  structure do
  end
end

We'll generate Train, ScheduledService, and Route models.

Let there be schema!

Now we construct our model as required, including its associations:

class Train < ActiveRecord::Base
  has_many :routes, :through => :scheduled_services
  has_many :scheduled_services
 
  structure do
    model_name           "British Rail 153 Series", :validates => :presence
    registration_number  "CA12512",                 :validates => { :format => /^\w+$/ }
    maximum_speed        120 # Kilometers per hour
    capacity             420 # Passengers (sitting and standing)
  end
end

Now that we're happy with our domain model descriptions, we want the database to store it. Normally this would involve writing migrations, then migrating our development database, then cloning out to our test database. With the following command however, we do it all in one swift motion:

> rake db:upgrade
==  CreateTrains: migrating ===================================================
-- create_table(:trains)
   -> 0.0009s
==  CreateTrains: migrated (0.0009s) ==========================================

This creates four fields, with the data type in the database inferred by the type of object passed to Migrant. So, the string literals (model_name, registration_number) will assume a varchar type, and the fixnums (maximum_speed, capacity) an int(8) type. There are several types you can use with Migrant, just use whichever object best represents what will be stored in that field. If there is no matching object type, Migrant will default to a string. You can also define your own types with Migrant, but we'll stick to the primitives for now.

Mock data

You're probably wondering why we're giving examples of the data we want to store, rather than explicitly stating the type as in a migration. There are two major reasons for this, firstly it serves as a descriptive example to future developers about what is being stored in your created fields (because let's face it, no matter how well you name your fields their purpose tends to be forgotten sometimes), and the other reason is that all models using Migrant will get a .mock() method, which will instantiate a filled in model using the example data you've given. This is somewhat similar to factories like Machinist and Factory Girl, but on simpler scale. Also included is a driver for Pickle if you're so inclined (see README).

> rails c
 
ruby-1.9.2-p180 :009 > Train.mock
 => #<Train id: nil, model_name: "British Rail 153 Series", registration_number: "CA12512", maximum_speed:"120", capacity:"420">
ruby-1.9.2-p180 :012 > Train.mock.save
 => true

Validations

It is recommended that you also specify validations in the Migrant DSL, rather than calling 'validates' seperately. This is just a case of not repeating yourself. The options you supply are pretty much sent as is onto ActiveRecord, but here are some different formats just for clarity:

structure do
  model_name           "British Rail 153 Series", :validates => :presence
  registration_number  "CA12512",                 :validates => [{ :format => /^\w+$/ }, :uniqueness]
  maximum_speed        120,                       :validates => { :numericality => { :greater_than => 0 } }
end
 
# Is equivalent to the following validations:
validates :model_name, :presence => true
validates :registration_number, :format =>  /^\w+$/
validates :registration_number, :uniqueness => true
validates :maximum_speed, :numericality => { :greater_than => 0 }

Associations

Let's complete our domain model:

class Route < ActiveRecord::Base
  has_many :scheduled_services
 
  structure do
    origin
    destination # Specifying no type generates a string by default
    timestamps # Behaves exactly like ActiveRecord migrations t.timestamps
  end
end

app/models/scheduled_service.rb:

class ScheduledService < ActiveRecord::Base
  belongs_to :train
  belongs_to :route
 
  no_structure # Alias for structure {}, simply implies this stores no fields of its own
end

PROFIT!

Run db:upgrade again and Migrant will detect and make the necessary changes:

> rake db:upgrade
==  CreateScheduledServices: migrating ========================================
-- create_table(:scheduled_services)
   -> 0.0008s
-- add_index(:scheduled_services, :train_id)
   -> 0.0003s
-- add_index(:scheduled_services, :route_id)
   -> 0.0004s
==  CreateScheduledServices: migrated (0.0016s) ===============================

==  CreateRoutes: migrating ===================================================
-- create_table(:routes)
   -> 0.0008s
==  CreateRoutes: migrated (0.0009s) ==========================================
Migrated. Now, cloning out to the test database.

Indexes

Migrant automatically indexes all variations of foreign keys. This might be considered premature optimization but in the current age of the giant cloud storage, database size is rarely a consideration.

FREEEOWL!

All done. Congratulate yourself on a job well done.