hasmanythrough – instant CRUD for Rails

Get Version 0.4.0

Instant look-see

Need many-to-many relationships in your app? Yes, we all do. All the time. What about free, instant CRUD – models, migrations and unit tests in an instant? BONUS: Controllers and their functional tests?

testapp> ruby script/generate has_many_through User Group Membership
  create  app/models/group.rb
  create  test/unit/group_test.rb
  create  test/fixtures/groups.yml
  create  db/migrate/001_create_groups.rb
  create  app/models/user.rb
  create  test/unit/user_test.rb
  create  test/fixtures/users.yml
  create  db/migrate/002_create_users.rb
  create  app/models/membership.rb
  create  test/unit/membership_test.rb
  create  test/fixtures/memberships.yml
  create  db/migrate/003_create_memberships.rb

Installing

myapp> gem install has_many_through_generator
Or download the gem directly from rubyforge.

Why?

If you want a model, a migration file, and a unit test file, you use the model generator. So if your object model consists of users and groups, with a memberships table in the middle to represent the many-to-many relationship, you have to do the following:

testapp> ruby script/generate model User
-- generates user.rb, 001_create_user.rb, user_test.rb
testapp> ruby script/generate model Group
-- generates group.rb, 002_create_group.rb, group_test.rb
testapp> ruby script/generate model Membership
-- generates membership.rb, 003_create_membership.rb, membership_test.rb
Add foreign keys to 003_create_membership.rb:
     t.add_column :group_id, :integer
     t.add_column :user_id, :integer
Add belongs_to to Membership class, in membership.rb:
class Membership < ActiveRecord::Base
    belongs_to :group
    belongs_to :user
end
Add has_many to User/Group classes, e.g.:
class User < ActiveRecord::Base
    has_many :memberships
    has_many :groups, :through => :memberships
end
It only takes a few minutes to get your models setup this way. Lovely.

But then you need to write test cases. Perhaps it takes 20-60 minutes to refactor some old unit tests. If you’re new to Rails, you probably don’t have any similar code nor do you know what test cases to add to your unit tests.

The has_many_through generator gives you all this for free (BONUS: secret free controllers and functional tests). What’s why you should use it.

Tutorial

Start with something simple: a new app, and some new models:

> rails testapp
> cd testapp
testapp> ruby script/generate has_many_through User Group Membership
(see above for sample output)
Now view the models (app/models/) and see that the appropriate has_many and belongs_to commands are generated for you. The generated code for the User class is just like above. We’ll view the unit tests in a moment, but they should all work from scratch:
testapp> mysqladmin -u root create testapp_development
testapp> mysqladmin -u root create testapp_test
testapp> rake migrate
-- create_table(:groups)
-- create_table(:users)
-- create_table(:memberships)
testapp> rake test:units
C:/InstantRails/ruby/bin/ruby -Ilib;test
"C:/InstantRails/ruby/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader.rb"
"test/unit/group_test.rb" "test/unit/membership_test.rb"
"test/unit/user_test.rb"
Started
......................
Finished in 0.626 seconds.

Let’s put some useful fields into the tables:

# db/migrate/001_create_groups.rb
class CreateGroups < ActiveRecord::Migration
  def self.up
    create_table :groups do |t|
      t.column :name, :string, :null => false
      t.column :email, :string, :null => false
    end
  end
...
end

# db/migrate/002_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :name, :string, :null => false
      t.column :date_of_birth, :date
    end
  end
...
end
Refresh the migration (take down the tables and rebuild them):
testapp> rake migrate VERSION=0
(in C:/InstantRails/rails_apps/testapp)
-- drop_table(:memberships)
-- drop_table(:users)
-- drop_table(:groups)

testapp> rake migrate
-- create_table(:groups)
-- create_table(:users)
-- create_table(:memberships)
Now update the model classes to add validation for the required fields:
# app/models/groups.rb
class Group < ActiveRecord::Base
  has_many :memberships
  has_many :users, :through => :memberships
  validates_presence_of :name
  validates_uniqueness_of :name
end

# app/models/users.rb
class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, :through => :memberships
  validates_presence_of :name, :email
  validates_uniqueness_of :email
end
Now run the unit tests again:
testapp> rake test:units
Started
...FF.............FF..
Finished in 0.599 seconds.

  1) Failure:
test_new(GroupTest) [./test/unit/group_test.rb:28]:
Group should be valid.
<false> is not true.

  2) Failure:
test_raw_validation(GroupTest) [./test/unit/group_test.rb:18]:
Group should be valid without initialisation parameters.
<false> is not true.

  3) Failure:
test_new(UserTest) [./test/unit/user_test.rb:28]:
User should be valid.
<false> is not true.

  4) Failure:
test_raw_validation(UserTest) [./test/unit/user_test.rb:18]:
User should be valid without initialisation parameters.
<false> is not true.
22 tests, 33 assertions, 4 failures, 0 errors

AHH! Our tests failed on us! That’s because we never told the tests that we expected some required fields, unique fields, and in general we’ve not given the tests any real data that matches our models.

So, let’s look at the unit tests for Group:

# test/unit/group_test.rb
class GroupTest < Test::Unit::TestCase
  fixtures :groups, :users, :memberships

  NEW_GROUP            =   {  } # e.g. {:name => 'Test Group', :description => 'Dummy'}
  REQ_ATTR_NAMES       = %w(  ) # name of fields that must be present, e.g. %(name description)
  DUPLICATE_ATTR_NAMES = %w(  ) # name of fields that cannot be a duplicate, e.g. %(name description)
...
end

The generated test files make it easy to setup the tests appropriate to your data model. They also already include the 3 sets of fixtures your generated tests require. Change the constants in each test case to:

# test/unit/group_test.rb
  NEW_GROUP            =   { :name => 'Test Group' }
  REQ_ATTR_NAMES       = %w( name )
  DUPLICATE_ATTR_NAMES = %w( name )

# test/unit/unit_test.rb
  NEW_USER             =   { :name => 'Test User', :email => 'test@user.com' }
  REQ_ATTR_NAMES       = %w( name email )
  DUPLICATE_ATTR_NAMES = %w( email )

And now our unit tests work perfectly.

testapp> rake test:units
Started
......................
Finished in 0.47 seconds.

Forum

http://groups.google.com/group/hasmanythrough

Licence

This code is free to use under the terms of the MIT licence.

Contact

Comments are welcome. Send an email to Dr Nic Williams.

Dr Nic, 22nd July 2006
Theme extended from Paul Battley