has_many :through checkboxes

8:37 PM EDT Wednesday, October 24 2007

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.

Posted in  | Tags Rails, Ruby

Comments Feed

1. very nicely explained. This is pretty much exactly what I was looking for.

# Posted By kajinski on Wednesday, November 07 2007 at 12:19 EST

2. This works brilliantly. A word to anyone who is trying to understand this if you're not actually dealing with users, groups and memberships: it makes things a lot easier if you just cut and paste this entire post into a text editor, then search and replace the terms 'user', 'group', and 'membership' with the actual entities from your system.

Plus, then the code will work in your application with minimal adjustment.

Thanks for the help Paul.

# Posted By Ade on Tuesday, December 04 2007 at 02:19 EST

3. Great solution! The only thing I miss is that the selection isn't remembered when the model doesn't validate. In the habtm solution, this still works..

# Posted By xinit on Thursday, December 27 2007 at 01:14 EST

4. Thank you very much for you post. It was exactly what I was looking for. I started learning Rails two weeks ago. I don't suppose someone could confirm that this is the correct `view` script:


<!-- users/_form.rhtml -->
<% form_for(@user) do |f| %>

<!-- ... -->

<%= hidden_field_tag "user[group_ids][]", "" %>
<% for group in group.find(:all) %>
<div>
<%= check_box_tag "user[group_ids][]",
group.id, @user.groups.include?(group) %>
<%= group.name %>
</div>
<% end %>

<!-- ... -->

<% end %>

# Posted By Peter on Monday, January 14 2008 at 05:33 EST

5. @Peter,

That looks good to me.

# Posted By Paul Barry on Tuesday, January 15 2008 at 08:00 EST

6. I appreciate the feedback Paul. For newbies like myself, the previous post contains an error. Please use the following:

<!-- users/_form.rhtml -->
<% form_for(@user) do |f| %>

<!-- ... -->

<%= hidden_field_tag "user[group_ids][]", "" %>
<!-- Use Group.find(:all) instead of group.find(:all) -->
<% for group in Group.find(:all) %>
<div>
<%= check_box_tag "user[group_ids][]",
group.id, @user.groups.include?(group) %>
<%= group.name %>
</div>
<% end %>

<!-- ... -->

<% end %>

# Posted By Peter on Tuesday, January 15 2008 at 09:28 EST

7. @Paul

In order to delete dependencies, shouldn't the membership model look more like this:

#app/models/user.rb
class User < ActiveRecord::Base
<!-- next line is different from original -->
has_many :memberships, :dependent => :destroy
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
end
end
end

# Posted By Peter on Tuesday, January 15 2008 at 10:43 EST

8. Thanks for this. Works perfectly for create/update a checkbox array!

# Posted By Andy on Tuesday, January 22 2008 at 01:54 EST

9. Great example!
But what happens if you extend memberships table with string field?

# Posted By Dean on Sunday, February 03 2008 at 01:31 EST

10. Great post Paul! The title matches the question in my head.

# Posted By Leandro on Sunday, February 10 2008 at 08:50 EST

11.

Hi,

I want to implement the many to many relation,using the HMT appproach,by defining the _ids method.

I have 3 tables companies,groups,titles.The companies & members table have many to many mapping,usinh has_many :through,titles being the join model.
In the Views file(.html.erb) I have,used following code:

<select name="title[company_id]" >
-----------------
--------------------
</select>

This causes only one value,being added to the database.

If I add the following code to the model file,(member.rb),

def companies_ids=(companies_ids)
titles.each do |tit|
tit.destroy unless company_ids.include? tit.company_id
end
company_ids.each do |company_id|
self.titles.create(:company_id => company_id) unless titles.any? { |d| d.company_id == company_id }
end
end
end



& use the following in the views file(.html.erb):

<select name="title[company_ids]" >
---------------------------
---------------------------
</select>

This does not populate the database for all values.

Please reply, as to where am I going wrong.



# Posted By Ruby_New on Tuesday, March 25 2008 at 09:46 EDT

12. @Paul,
This is not working for me. What is the "reload" call meant to do? Reset the group_ids attribute to nil?

When I run this in the console, using the same commands shown in your article, the group_ids attribute persists into the next call to update_groups. This causes trouble when we get to:

foo.update_attributes(:name => 'foo')

At this point, group_ids contins [4]. The method then proceeds to delete the memberships to groups 1 and 3.

I tried adding an assignment "group_ids = nil" at the end of the method, but this doesn't seem to have any effect. That confuses me...

# Posted By Charlie on Sunday, April 27 2008 at 06:50 EDT

13. @Charlie

reload will reload the the object and it's associated objects into memory from the database. The value of group_ids is not affected by reload.

I just tried the example with Rails 2.0 and you are right, memberships 1 and 3 get deleted. I though that I tested this when I wrote the article, so either something in Rails 2.0 has changed, or maybe the code was broken all along. Probably the later :)

Anyway, you were on the right track with fixing the problem. You set group_ids = nil after the reload statement. But the trick is that just saying group_ids = nil has no effect. Inside a method call, that just creates a new local variable and assigns nil to it. In order to set the group_ids attribute of the object to nil, you must do self.group_ids = nil. In Ruby, any time you are calling a setter, as we are in this case, group_ids=, then you must prefix it with self, or a local variable will be created, instead of calling the setter.

# Posted By Paul Barry on Sunday, April 27 2008 at 08:06 EDT

14. Ah, much thanks! Missed that one. Great post by the way. If there's a better source of info on what to me ought to be a very obvious basic thing one should want to do with rails, I have not found it.

-Charlie

# Posted By Charlie on Sunday, April 27 2008 at 09:12 EDT

15. Thanks Charlie. I updated the post to include the self.group_ids = nil. For what it's worth, I find myself still using has and belongs to many for things that are truly just a set of checkboxes. In other words, I don't think HABTM is going away and it is still useful for some use cases, specifically, when all you want is a list of checkboxes. If you need to store more attributes in the join table, then switch to HMT, but when that becomes the case, it's unlikely your interface will still just be a set of checkboxes.

# Posted By Paul Barry on Monday, April 28 2008 at 07:26 EDT

16. I put this code into my project and for some reason all I'm getting is it'll delete whatever "memberships" the current user already has, and then re-add them... ( on an update... on a create, it skips the loop )

So it's calling the object's group_ids instead of the parameter being passed by the forum...

Any idea what could cause this?

# Posted By sw0rdfish on Monday, April 28 2008 at 02:38 EDT

17. @sw0rdfish

Are you sure group_ids is set to an array of strings? If you set group_ids to array of integers, that will cause it to delete and re-add. I would suggest just doing a little debugging in the method. If it's deleting records it shouldn't be, group_ids.include?(m.group_id.to_s) must be false for some reason.

# Posted By Paul Barry on Monday, April 28 2008 at 04:11 EDT

18. Thanks Paul,

I checked around and I'm pretty sure it's returning strings... Its' weird because if I have no Memberships ( Groups ) already defined, I don't think it even goes into the loop... even though I choose two new ones.

And if the user already has groups, which I call roles, the user is removed from, and then re-added to the same group he was already a member of, regardless of which boxes I check off.

I should have stuck with HABTM, but now I'm curious.

# Posted By sw0rdfish on Monday, April 28 2008 at 04:32 EDT

19. My view for the checkboxes BTW...

<%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role), :class => 'checkbox' %>

# Posted By sw0rdfish on Monday, April 28 2008 at 04:33 EDT

20. from the console...

>> user.update_attributes(:role_ids => ['1', '2'])
=> true
>> puts user.permissions.collect{|p| "#{p.id} => #{p.role.rolename}\n" }
=> nil
>> user.update_attributes(:role_ids => [1,2])
=> true
>> puts user.permissions.collect{|p| "#{p.id} => #{p.role.rolename}\n" }
=> nil

# Posted By sw0rdfish on Monday, April 28 2008 at 04:41 EDT

21. and to conitune to spam... if the user is a member of a group twice.... it adds the same group twice again.

# Posted By sw0rdfish on Monday, April 28 2008 at 04:53 EDT

22. The problem was related to RESTful Authentication... I had to add the role_ids to the "attr_accessible" in the user model... major DUH there on my part...

I love wasting a day on stuff like that...

# Posted By sw0rdfish on Tuesday, April 29 2008 at 12:38 EDT

23. Similar, but older, was posted on my blog. I note that you're using strings where integers would be more strict (and allow for a natural use of the console/other default methods for creation of memeberships).

http://euphemize.net/blog/archives/2007/06/04/learning-rails-many-to-many-relationships/

# Posted By Joel on Tuesday, April 29 2008 at 09:20 EDT

24. @Joel,

You are probably right, integers might make more sense. I was just doing strings because that is what they will be when they come in as request parameters. It would be easy enough to, right after the unless, just do group_ids = groups_ids.map(&:to_i), which would allow for either strings or integers.

# Posted By Paul Barry on Wednesday, April 30 2008 at 01:13 EDT

25. I'm running into a weird error when I try to use the code above:

undefined method `role_ids=' for #<User:0x46a1c5c>

This happens during the update_attributes(params[:user]) part of the update method.

Any ideas as to what would be causing this?

# Posted By Ikazuchi on Thursday, May 01 2008 at 05:02 EDT

26. thank-you.

# Posted By Bartosz Pietrzak on Tuesday, June 10 2008 at 10:20 EDT

27. I think hmt_ids may soon be available.

http://dev.rubyonrails.org/ticket/11516

At least, that's what it sounds like.

# Posted By Jason Boxman on Monday, June 16 2008 at 11:06 EDT

28. Hi,

Great tutorial, thanks!

Is there anything in particular I need to do to make:
@user.groups.include?(group)
work?

My tables are calls
- area_codes
- markets
- area_codes_markets

Adding a new market with a few area_codes works.

Editing a market fails however. I get the error:

NameError in Markets#edit
Showing markets/_form.html.erb where line # raised:
uninitialized constant Market::AreaCodes

I traced it down to:
@market.area_codes.include?(area_code)
because if I replace that snippet by either 'true' or 'false' the edit page works.

Might there be a problem with the usage of an underscore ('_') in the table name (i.e. area_codes)?

Thanks for any help you can give me!

# Posted By Birgit Arkesteijn on Tuesday, July 01 2008 at 12:32 EDT

29. Never mind, I found the problem.
I used plurals in my "belongs_to" statement in
my model for area_codes_markets (AreaCodesMarket).
I've wasted many a day chasing problems due to the wrong plural or singular.

# Posted By Birgit Arkesteijn on Tuesday, July 01 2008 at 01:14 EDT

30. Thanks for this great resource. I have tried the example, and I can see a membership I put in via the dB, but my edits of a user do not reflect any changes to the groups I make via the checkboxes. Is there something I am missing?

I just added the checkbox code to the existing user edit page I had. Like I said, if I add a membership by hand, I see it in the edit page (as a checked box), but when I change a box's status it does not update the dB...Not sure why not.

# Posted By ccecil on Saturday, August 02 2008 at 02:41 EDT

31. OK - found my isue thanks to a tail of the development log. Like many people I am using restful_authentication (the plugin), and by default the controller uses attr_accessible for security. If you (like me) are just getting into Rails and Ruby, watch out for this. You must add the group_ids to the list of accessible parameters to get the update to work.

# Posted By cecil on Saturday, August 02 2008 at 03:29 EDT

32. Thank you for the tip. I've developed an alternate approach that works with form_for and preservers the selections after validation fails:

http://jarrettcolby.com/articles/2-virtual-attributes-for-many-to-many-checkboxes-in-ruby-on-rails

Your approach definitely has the advantage of simplicity. It would be really great if someone could invent a way to do these checkboxes that's simple, retains selections when validation fails, and works with many-to-many. Perhaps a plugin that uses method_missing and alias_method_chain.

# Posted By Jarrett on Thursday, September 11 2008 at 07:25 EDT

33. Could someone explain how to do this with a select box instead of checkboxes? With the user only being allowed in one group?

e.g. In the create new user view:

• Choose group from select box showing groups
• Enter name

Thanks

# Posted By Paul on Monday, October 13 2008 at 05:34 EDT

34. Ahoy,

I have a solution for has_many associations with a single select box and a list of checkboxes to store the associations.

It uses client-side javascript, no AJAX, and the code is simpler than Ryan Bates'complex forms.


Take a look at: http://clair.ro/rc/2008/10/13/has_many-with-select-box-and-checkboxes/

# Posted By cs on Monday, October 13 2008 at 10:36 EDT

35. Thank you! Great tutorial. Don't forget to add the category_ids to attr_accessible.

# Posted By P.J. on Saturday, November 29 2008 at 05:04 EST

36. I'm over a year late on this post but damn, this just saved the day today.

Paul, your code is very clear, the write up is concise and clear as well... just a great post.

Thanks for taking the time to publish it AND more importantly, thanks for UPDATING the post when people find bugs.

SO many authors do not do this and it leads to people slamming their face on the desk... my desk is glass and this could be dangerous.


# Posted By Z on Saturday, December 20 2008 at 05:42 EST

Add a Comment