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.rb
config.cache_classes = true config.eager_load = true
It was necessary for
ActiveRecord::Base.descendants
to 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.txt
andrails4.txt
file.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
I used
diff
to comparerails3.txt
andrails4.txt
files. 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
join_table
explicit: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: