The Underused collection_singular_ids Method

Comments

I was talking to someone a few weeks back, and he was jumping through a bunch of hoops to create fields in his form that allowed him to set associated objects. “You know about the collection_singular_ids methods, right?” I asked. “No, please explain,” he said. So I did. I wanted to write a quick post here, as well, in case it might help someone else.

The problem

Let’s say you have the following models:

class Post < ActiveRecord::Base
  has_and_belongs_to_many :categories
end
 
class Category < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

In your form, you have fields for your post title and body, and you want to add a way for a user to assign categories to his posts, as well. It’s actually way, way simpler than it often seems at first glance, thanks to methods that ActiveRecord has generated for a very long time on *_many associations: collection_singular_ids.

The solution

Now, in true ActiveRecord metaprogramming fashion, “collection_singular” is a placeholder. If a post has_and_belongs_to_many categories, it will have methods named category_ids and category_ids=. The first returns an array of the ids of all associated objects. The second, much more usefully, allows you to set the associated objects just by supplying an array of ids.

For example, in the console:

ruby-1.9.2-p0 > p = Post.first
=> #<Post id: 1, title: "A Title", body: "A body",
   created_at: "2010-10-02 18:02:36",
   updated_at: "2010-10-02 18:03:01"> 
ruby-1.9.2-p0 > p.categories
=> [#<Category id: 2, name: "Programming",
    created_at: "2010-10-02 17:57:00",
    updated_at: "2010-10-02 17:57:00">] 
ruby-1.9.2-p0 > p.category_ids
=> [2] 
ruby-1.9.2-p0 > p.category_ids = [1,3]
=> [1, 3] 
ruby-1.9.2-p0 > p.categories
=> [#<Category id: 1, name: "Rails",
   created_at: "2010-10-02 17:56:52",
   updated_at: "2010-10-02 17:56:52">,
   #<Category id: 3, name: "Ruby",
   created_at: "2010-10-02 17:57:17",
   updated_at: "2010-10-02 17:57:17">]

The results are immediately persisted in the database, so you don’t have to call Post#save afterwards.

Since all you’re doing is supplying an array of integers to an attribute writer on your model, this means something as simple as…

<%= f.label :category_ids, "Categories" %><br />
<%= f.collection_select :category_ids, Category.all, :id, :name,
                        {}, :multiple => true  %>

…will do the trick.

Of course, everyone hates multiple selection boxes, so let me make a gratuitous plug for MetaSearch’s collection_checks method:

<%= f.label :category_ids, "Categories" %><br /> 
<% f.collection_checks :category_ids, Category.all,
                       :id, :name do |check| %>
  <%= check.box %> <%= check.label %><br />
<% end %>

Much better, don’t you agree?

Example Image

comments powered by Disqus