mokacoding

unit and acceptance testing, automation, productivity

Rails: adding a new has_many association to an existing model

This post shows how to evolve an existing schema adding new models and association in Ruby on Rails. I did this today at work, had to put together pieces from the Rails Guides and other resources.

The starting point is a schema with a single items table. We want to have a level system, where each level is made up by a group of challenges, and each challenge contains a number of items.

Note: I'm using rspec and shoulda to write the tests.

Step 1 - Create the Challenge model

Creating a new empty model is easy, just run

rails g model challenge

and the resulting migration

rake db:migrate RAILS_ENV=development

Step 2 - Add the association "challenge has many items"

We want to have a model that makes these tests pass:

describe Challenge do
  it { should have_many :items }
end

describe Item do
  it { should belong_to :challenge }
end

The first thing would be to write a migration, but there is no way to generate a migration for an association with the rails generate migration command. So we have to do it manually, and then write the migration to update the db and schema.

class Challenge < ActiveRecord::Base
    has_many :items
end

class Item < ActiveRecord::Base
    belongs_to :challenge
end
rails g migration AddItemsAssociationToChallenge

This is the code to put in the resulting migartion file

class AddItemsAssociationToChallenge < ActiveRecord::Migration
  def self.up
      add_column :items, :challenge_id, :integer
      add_index 'items', ['challenge_id'], :name => 'index_challenge_id' 
  end

  def self.down
      remove_column :items, :challenge_id
  end
end

Finally let's run rake db:migrate and rspec (because we're using binstub aren't we?) and everything should be fine.

Step 3 – The Levels

The process for the levels will be the same as before, a good way to commit the steps to memory. We want this specs to pass:

describe Level do
    it { should have_many :challenges }
end

describe Challenge do
    it { should belong_to :level }
end

So we generate a migration

rails g migration AddChallengesAssociationToLevel

and we write this inside it

class AddChallengeAssociationToLevel < ActiveRecord::Migration
  def self.up
      add_column :challenges, :level_id, :integer
      add_index 'challenges', ['level_id'], :name => 'index_level_id' 
  end

  def self.down
      remove_column :challenges, :level_id
  end
end

finally we cannot forget to manually update our models

class Level < ActiveRecord::Base
    has_many :challenges
end

class Challenge < ActiveRecord::Base
    has_many :items
    belongs_to :level
end

That's all. Nothing incredibly hard, but still not obvious for someone who mainly writes Objective-C. Happy coding!

Vote on Hacker News