hasmanythrough – instant CRUD for Rails
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_generatorOr 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.rbAdd 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 endAdd has_many to User/Group classes, e.g.:
class User < ActiveRecord::Base has_many :memberships has_many :groups, :through => :memberships endIt 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 ... endRefresh 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 endNow 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