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
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.cache_classes = true config.eager_load = true
It 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
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 end
rails4.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 :products
All I had to do, was to make the
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: