As you probably already know, Rails has 2 methods of handling Many-to-Many relationships,
has_and_belongs_to_many
(HABTM) and
has_many_through
(HMT). Ryan Bates does a great job explaining the difference between the 2 in Railscast #47: Two Many-to-Many. He does seem to indicate the HABTM is effectively deprecated:
Now in the other cases you might want to go with has and belongs to many...you might, but that's kind of going quickly out of date and it has some limitations that you might experience further on in development, so if you are the least bit unsure on which way to go, it's usually better to go with has many through, because that it much more flexible.
Ok, so what if I do want to use simple checkboxes in my interface, but I don't want to use the soon-to-be out of date, inflexible method?
Basically, all you need to do is implement an
_ids
method for the association, and then you will be good to go. The
_ids
method is automatically generated for you when you use HABTM, but since HMT is often used with models that aren't just simple join associations, there is no automatic
_ids
method generated for you.
So let's create an example app where we have a User model which has many Groups through the Membership model:
$ rails example
$ cd example
$ mysqladmin -u root create example_development
$ script/generate scaffold_resource user name:string
$ script/generate scaffold_resource group name:string
$ script/generate scaffold_resource membership user_id:integer group_id:integer
$ rake db:migrate
Ok, now we've got our DB and our models, so we just need to set up the associations in each model:
#app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
#app/models/group.rb
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
And in the User model, we'll add the associations as well as an implementation for the group_ids method:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
attr_accessor :group_ids
after_save :update_groups
#after_save callback to handle group_ids
def update_groups
unless group_ids.nil?
self.memberships.each do |m|
m.destroy unless group_ids.include?(m.group_id.to_s)
group_ids.delete(m.group_id.to_s)
end
group_ids.each do |g|
self.memberships.create(:group_id => g) unless g.blank?
end
reload
self.group_ids = nil
end
end
end
So what's going on here is that first we define an attribute to hold the
group_ids
. Then, we define a method that will get called after this model is saved. In that callback, first check to see if
group_ids
is nil, because if it is, we're going to do nothing. Then we iterate through each membership that this record has, delete it if it's
group_id
is not in our new array of
group_ids
. Then we remove the
group_id
from the array, so that anything we have left in the
group_ids
array after we've gone through all the existing memberships are groups that we should create new memberships for, for this user.
So let's see if this works. So we log into the console and first create some Groups to work with:
$ script/console
Loading development environment.
>> ('A'..'E').each{|n| Group.create!(:name => n) }
=> "A".."E"
Now we can create a User with some groups:
>> foo = User.create!(:name => 'foo', :group_ids => ['1','2','3'])
=> #<User:0x30e2c90...
>> puts foo.memberships.collect{|m| "#{m.id} => #{m.group.name}\n" }
1 => A
2 => B
3 => C
As you can see, we specified 3 groups that we wanted this user to have membership in. So there are 3 membership records created. Now let's take this user out of group B and put them in group D:
>> foo.update_attributes(:group_ids => ['1','3','4'])
=> true
>> puts foo.memberships.collect{|m| "#{m.id} => #{m.group.name}\n" }
1 => A
3 => C
4 => D
Notice how A and C still have the same membership id. This is important, because you don't want to just delete all the memberships and create new ones, in case there are other attributes on the membership. Of course if there are other attributes, you probably won't be using checkboxes to edit them, but you get the idea. Let's just check a couple more things to make sure this method works. First, we want to make sure if we update the model without specifying the group_ids that it remains unchanged:
>> foo.update_attributes(:name => 'foo')
=> true
>> puts foo.memberships.collect{|m| "#{m.id} => #{m.group.name}\n" }
1 => A
3 => C
4 => D
Ok, good, and last but not least, if we set group_ids to an empty array, then we want to make sure all of the memberships are deleted:
>> foo.update_attributes(:group_ids => [])
=> true
>> puts foo.memberships.collect{|m| "#{m.id} => #{m.group.name}\n" }
=> nil
Now we just need to update the views in our scaffolding to put checkboxes on the form, which I explained how to do in a previous article called HABTM Checkboxes.
[url]http://paulbarry.com/articles/2007/10/24/has_many-through-checkboxes[/url]