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'

CatalogProduct < ActiveRecord::Base
  has_and_belongs_to_many :catalog_categories,
    join_table: 'catalog_categories_catalog_products'

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 and rails4.txt file.

    ActiveRecord::Base.descendants.sort_by(&:name).each do |klass| do |_name, refl|
        refl.macro == :has_and_belongs_to_many
      end.each do |name, refl|
        if Rails::VERSION::MAJOR == 3
          puts  [
          puts [
  • I used diff to compare rails3.txt and rails4.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:

You might also like