mbigras
mbigras

Reputation: 8055

Rails changing table name in has_and_belongs_to_many

I have two models StageBatch and StageItem:

class StageItem < ApplicationRecord
  has_and_belongs_to_many :stage_batches
end

class StageBatch < ApplicationRecord
  has_and_belongs_to_many :stage_items
end

With the following migration:

class CreateStageBatchesAndStageItems < ActiveRecord::Migration[5.0]
  def change
    create_table :stage_items do |t|
      t.string :name
      t.timestamps
    end

    create_table :stage_batches do |t|
      t.timestamps
    end

    create_table :stage_batches_and_stage_items, id: false do |t|
      t.belongs_to :stage_item, index: true
      t.belongs_to :stage_batch, index: true
    end
  end
end

When I try to access a stage_batches stage_items I get the following error:

irb(main):002:0> StageBatch.first.stage_items
  StageBatch Load (0.2ms)  SELECT  "stage_batches".* FROM "stage_batches" ORDER BY "stage_batches"."id" ASC LIMIT ?  [["LIMIT", 1]]
  StageItem Load (0.4ms)  SELECT "stage_items".* FROM "stage_items" INNER JOIN "stage_batches_items" ON "stage_items"."id" = "stage_batches_items"."stage_item_id" WHERE "stage_batches_items"."stage_batch_id" = ?  [["stage_batch_id", 1]]
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such table: stage_batches_items: SELECT "stage_items".* FROM "stage_items" INNER JOIN "stage_batches_items" ON "stage_items"."id" = "stage_batches_items"."stage_item_id" WHERE "stage_batches_items"."stage_batch_id" = ?

Upvotes: 1

Views: 1080

Answers (1)

K M Rakibul Islam
K M Rakibul Islam

Reputation: 34338

You have to follow the Rails naming convention for naming the joining table while using has_and_belongs_to_many. Alternatively, you can specify a joining table name using join_table option.

In your particular case, change the stage_batches_and_stage_items's name to stage_batches_stage_items. By default, the joining table has both table names (pluralized) in lexical order.

From the Rails guide [Section 3.3.2]:

If you create a has_and_belongs_to_many association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the :join_table option, Active Record creates the name by using the lexical book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering.

I would recommend avoiding has_and_belongs_to_many if this is not a hard requirement due to it's limitations (e.g., you can't add any extra columns to this joining table.) Instead, use has_many :through association. In that case, you can customize the joined table name easily. Also, you can add extra columns to the joining table as you need.

Here is how the code will look like using has_many :through association:

class StageItem < ApplicationRecord
  has_many :stage_item_batches
  has_many :stage_batches, through: :stage_item_batches
end

# joining model
class StageItemBatch < ApplicationRecord
  belongs_to :stage_item
  belongs_to :stage_batch
end

class StageBatch < ApplicationRecord
  has_many :stage_item_batches
  has_many :stage_items, through: :stage_item_batches
end

The migrations for original two models will stay the same. You just need a migration for the joining model:

rails g model StageItemBatch stage_item:references stage_batch:references

Upvotes: 1

Related Questions