Safely migrating hasandbelongstomany associations to Rails 4
… and check why 5600+ Rails engineers read also this
Safely migrating has_and_belongs_to_many associations to Rails 4
During recent days I’ve been migrating a senior Rails application from Rails 3 to Rails 5. As part of the process, I was dealing with has_and_belongs_to_many associations.
As you can read in the official migration guide
Rails 4.0 has changed to default join table for has_and_belongs_to_many relations to strip the common prefix off the second table name. Any existing has_and_belongs_to_many relationship between models with a common prefix must be specified with the join_table option. For example:
CatalogCategory < ActiveRecord::Base
has_and_belongs_to_many :catalog_products,
join_table: 'catalog_categories_catalog_products'
end
CatalogProduct < ActiveRecord::Base
has_and_belongs_to_many :catalog_categories,
join_table: 'catalog_categories_catalog_products'
end
The application that I was working on has around 50 has_and_belongs_to_many associations in a codebase and I did not want to check manually if the join_table was properly inferred or not (and the tests don’t cover everything in the app).
I decided to use built-in Rails reflection mechanism for associations and check it. Here is how:
I temporarily (for the time of running next script) set the code to eager load in
config/development.rbconfig.cache_classes = true config.eager_load = trueIt was necessary for
ActiveRecord::Base.descendantsto find out all descending classes. They needed to be loaded.I executed the same script on Rails 3 and Rails 4 and saved its output in
rails3.txtandrails4.txtfile.ActiveRecord::Base.descendants.sort_by(&:name).each do |klass| klass.reflections.select do |_name, refl| refl.macro == :has_and_belongs_to_many end.each do |name, refl| if Rails::VERSION::MAJOR == 3 puts [ klass.name, name, refl.options[:join_table] ].inspect else puts [ klass.name, name, refl.join_table ].inspect end end endI used
diffto comparerails3.txtandrails4.txtfiles. The output looked like:< ["Bundle", :products, "bundles_products"] --- > ["Bundle", :products, "discounts_products"]Surprisingly I did not find any difference related to exactly what the changelog was talking about. But I still found a difference and the default was inferred differently due to the inheritance.
class Bundle < Discount has_and_belongs_to_many :productsAll I had to do, was to make the
join_tableexplicit:class Bundle < Discount has_and_belongs_to_many :products, join_table: "bundles_products"As this keeps working properly in Rails 3 (it’s just explicit over implicit), I could deploy this change to production even before upgrading Rails.
If you are interested in the ability to dynamically examine the associations and aggregations of Active Record classes and objects read more about the available methods in ActiveRecord::Reflection::ClassMethods documentation.
If you enjoyed that story, subscribe to our newsletter. We share our everyday struggles and solutions for building maintainable Rails apps which don’t surprise you.
Also worth reading: