Monday, August 29, 2011

Associating has_many relationships in Rails 3 using checkboxes

(Not checkboxes, in my case, but a checkbox example is easy, and more common.)

Originally I thought I needed accepts_nested_attributes_for, but that seems to be mostly for when we're creating the related objects, which I'm not--I need to save relationships to existing objects.

My example (github) uses a simple product/category relationship. We need to get a product's categories, we need to get a category's products. In the olden days, we would have used habtm; this example uses has_many:through. We'll use checkboxes to associate categories on the product's form page.

A number of posts were helpful during this process, although none was an SSCCE (at least to my new-again-to-Rails eyes). I don't know if I ended up doing this in the (or a) Rails Way; feedback is welcome. The nutshell solution for me ended up boiling down to the following:

* I didn't need accepts_nested_attributes_for (I'm referencing, not creating).
* I needed attr_accessible to allow mass-assignments to the category IDs as per this post.
* This stackoverflow question provided form element hints.
These posts pointed me away from habtm, although I'm still fuzzy on why (not _why).
* The second post above also reinforced form element naming.

(Note: I became uncertain about needing attr_accessible; the example works without it. In my original project it didn't, but it's likely I screwed something up the first time around and need to revisit it.)

Here's the Product, Category, and ProductCategory models, in all their tiny little glory.

class Product < ActiveRecord::Base
  validates :name, :presence => true
  has_many :product_categories
  has_many :categories, :through => :product_categories
end

class Category < ActiveRecord::Base
  validates :name, :presence => true
  has_many :product_categories
  has_many :products, :through => :product_categories
end

class ProductCategory < ActiveRecord::Base
  belongs_to :product
  belongs_to :category
end

The migrations for Product and Category are what we'd expect. The mapping table migration is similarly trivial.

class CreateProductCategoriesTable < ActiveRecord::Migration
  def self.up
    create_table :product_categories, :id => false do |t|
      t.references :product
      t.references :category
    end
    add_index :product_categories, [:product_id, :category_id]
    add_index :product_categories, [:category_id, :product_id]
  end

  def self.down
    drop_table :product_categories
  end
end

The checkboxes are created with the snippet below. It's kind of "manual" this way, there's likely a cleaner way using stock Rails.

<% Category.all.each do |cat| %>
  <%= cat.name %>
  <%= check_box_tag :category_ids,
                    cat.id,
                    @product.categories.include?(cat),
                    :name => 'product[category_ids][]' %>
  

<% end %>

That's it: I suspect my original frenzied attempts, spinning through methodologies, got things out-of-sync and cost me a fair amount of time. Not needing attr_accessible is probably because I'm not using nested attributes. I'm not sure what's wrong with habtm. I'm not sure if there are performance penalties for using mapping classes. Lots to explore, which is both good and bad.

4 comments:

Ehud said...

That was very helpful, thanks :)

rahool said...

Nice Article!,properly explains has_many :through association ..

Unknown said...

Can u explain to me , how i can put a dropdown instead of a checbox

elioncho said...

This doesn't works in Rails 3.2.