Ruby on Rails
has_and_belongs_to_many

ActiveRecord allows you to link independent, but related tables in a many-to-many association mapping. This many-to-many mapping is made using a special mapping table.

Examples of this:

Tips and Tricks

If you create a new instance of a model that is participating in a HABTM relationship, you must save it first before adding relationships. For example:


product = Products.new(params[:product])
if product.save
  product.options << Option.find_by_name("red")
end

The reason is that Rails needs product.id to create the relationship.

Limitations

has_and_belongs_to_many is great for what it does, but it is only adequate for simple many-to-many relationships. If your intermediary table needs to track additional data, then you may instead want to use ThroughAssociations instead.

HABTM works with this example:


PRODUCTS
id
name

OPTIONS
id
name

OPTIONS_PRODUCTS
option_id
product_id

But not with this example:


PRODUCTS
id
name

OPTIONS
id
name

OPTIONS_PRODUCTS
option_id
product_id
price # (Notice the difference)
price_level_id # (Or, to make things more complex…)

In the second example, the intermediary table needs to track more than just the associations – it also needs to be readable and writable for a price field, or for a price_level_id foreign key.

What is the best way to handle this?

edit: use :through associations as seen [here][ThroughAssociations].

You could use push_with_attributes as shown here but this has been deprecated since 1.2.

Or, you can use the above but add an ‘id’ field to OPTIONS_PRODUCTS, thus making it an ActiveRecord in its own right. Rails doesn’t care, and will cheerfully maintain the correct habtm – but you can now change price (etc) by accessing a ProductOption model (say)…


class ProductOption << ActiveRecord::Base
set_table_name ‘OPTIONS_PRODUCTS’

def self.set_price(product, option, price) x = self.find(:first, :conditions = ‘product_id = ? and option_id = ?’, product.id, option.id]) x.price = price x.save end

end


You’ll want to add error handling of course.

Tips

The names of the join table must have the models in alphabetical order. Eg _options_products_
Using the opposite, ie. _products_options_, will throw an “ActiveRecord::StatementInvalid” error.

- That’s convention over configuration! (pity the fool)

When you create your habtm table, make sure to leave out the “id” column. Otherwise, the habtm join table id will be populated to contain the id of your joined model, not an auto-inc number. This, of course, leads to trouble; some people have even thought it was a bug.

To overcome it, create your custom. join tables like so, leaving the id column out.


class RelationsTranzaction < ActiveRecord::Migration
  def self.up
    create_table :relations_tranzaction, :id => false do |t|
      t.column :invoice_id, :integer
      t.column :payment_id, :integer
    end
    add_index :relations_tranzaction, [:invoice_id]
    add_index :relations_tranzaction, [:payment_id]
    
  end

  def self.down
    drop_table :relations_tranzaction
  end
end

And alternative to leaving the id field out of the join table is to use a :select in the habmt. This ensures the id of the join record doesn’t end up in the model.

has_and_belongs_to_many :invoices, :select => 'invoices.*'

See also: