Intro

I recently came across a couple of caveates when working with has_and_belongs_to_many relationships with Rails. This post covers them and how to solve or work around them.

Software Versions

  • Rails - 6.0.3.4

Many to Many Relationship Type

When creating a many to many relationship, you can use two different types. A direct relationship between two models or an inderect relationship between an intermediate model. The docs cover this really well, so I wont rehash it here.

For my examples I am using a direct relationship between two models. This utilizes a join table to join the models together.

Relationship Naming

When creating a relationship between two tables the order of the associatied table names matter.

Lets say we want to create a relationship between the tenants and tags tables.

If you use a generator to create a join table, for example: rails generate migration TenantsTags you will end up with a schema that looks something like the below.

file
 # db/schema.rb

create_table "tenants_tags", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.uuid "tenant_id"
    t.uuid "tag_id"
    t.index ["tag_id"], name: "index_tenants_tags_on_tag_id"
    t.index ["tenant_id"], name: "index_tenants_tags_on_tenant_id"
  end

Notice how tenants comes before tags in the join table name?

If you try to add any tags to a tenant you will get an error similar to the below.

error
Caused by:
PG::UndefinedTable: ERROR:  relation "tags_tenants" does not exist
LINE 8:  WHERE a.attrelid = '"tags_tenants"'::regclass

Why is that? The table absolutely exists in the schema definition. Or does it? If you look closely at the error you will see that rails is looking for tags_tenants not tenants_tags .

After some digging I found this note in the docs .

Unless the join table is explicitly specified as an option, it is guessed using the lexical order of the class names. So a join between Developer and Project will give the default join table name of “developers_projects” because “D” precedes “P” alphabetically

- https://api.rubyonrails.org/v6.1.0/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many

Essentially, you need to name the join table names in alphabetical order. Reverse the join table name around to tags_tenants and it will work as expected.

Model Naming

For this example I have a direct many to many relationship between the vrfs and tags tables.

The next caveate I hit was related to Model naming. I have a vrf model and to stop rails from naming it weird, I have some inflection rules.

file
# config/initializers/inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'vrf', 'vrfs'
  inflect.acronym 'VRF'
  inflect.acronym 'VRFs'
end

This impacted rail's ability to find the vrf table from the tags table.

In the below error notice the Capitalized Vrf rather than the uppercase VRF

error
NameError: uninitialized constant Tag::Vrf

To work around this you need to explicitly tell the tag model the class name of the vrf model in the has_and_belongs_to_many association.

file
# app/model/tags.rb

class Tag < ApplicationRecord
  has_and_belongs_to_many :vrfs, class_name: "VRF"
end

After that, the associations work as expected.

Outro

In this post I covered a couple of the caveats I encountered while working with the has_and_belongs_to_many association in Rails. This will help future Brad. If you are reading this, it may help you too.

# rails