Convert has_and_belongs_to_many to a has_many :through association

So there are plenty of resources out there to learn how to use has_many :through associations.

I followed them over and over again but couldn’t get my code to work. I knew I had the basic structure setup correctly, since the examples are pretty straightforward, and the concept is not difficult. My has_and_belongs_to_many code originally looked like this:

class Soda < ActiveRecord::Base
  has_and_belongs_to_many :distributors
end
 
class Distributor < ActiveRecord::Base
  has_and_belongs_to_many :sodas
end

Of course there was also a many-to-many join table migration:

class DistributorsSodasJoinTable < ActiveRecord::Migration
  def self.up
    create_table :distributors_sodas, :id => false do |t|
      t.column :soda_id, :int
      t.column :distributor_id, :int
    end
  end  
 
  def self.down
    drop_table :distributors_sodas
  end
end

This works quite nicely:

>> Soda.find(1).distributors
=> []

Later found that I needed to add attributes in the join table to associate extra fields on the Distributors <-> Sodas relationship. has_and_belongs_to_many does not have a Rails way to access those extra fields in the join table. I’ve successfully done it through SQL, but much guilt and remorse lead me to finally learn has_many :through.

This was my best initial attempt:

class Soda < ActiveRecord::Base
  has_many :distributors_sodas
  has_many :distributors, :through =>; :distributors_sodas
end
 
class Distributor < ActiveRecord::Base
  has_many :distributors_sodas
  has_many :distributors, :through => :distributors_sodas
end
 
class DistributorsSodas < ActiveRecord::Base
  belongs_to :soda
  belongs_to :distributor
end

All goes well until I try to do a quick test:

>> Soda.find(1).distributors
NameError: uninitialized constant Soda::DistributorsSoda
...
        from (irb):4

Umm… what? I never tried to instantiate an object of the type Soda::DistributorsSoda. Instead, I was simply trying to use the DistributorsSodas ActiveRecord object, right?

It turns out that has_many :through (apparently) can’t handle using the join tables created by has_and_belongs_to_many. Its just a naming issue - has_many :through will work fine using a one-to-many join table like distributor_sodas (note the missing ’s’ on distributor). If you need a many-to-many join, you have to rename the table to fix the (pluralization?) problem. I deleted the DistributorsSodas model and created the Store model.

class Soda < ActiveRecord::Base
  has_many :stores
  has_many :distributors, :through => :stores
end
 
class Distributor < ActiveRecord::Base
  has_many :stores
  has_many :sodas, :through => :stores
end
 
class Store < ActiveRecord::Base
  belongs_to :soda
  belongs_to :distributor
end

This proved a much better result:

>> Soda.find(1).distributors
=> []

In the end, the association naming convention actually make more sense. I was bummed to have to change the table/model names though.

Please comment if you know how to create the association without changing the model name.