Let’s say we have “products” and we want to prepare “kits” of those products. Kits are nothing but the group of the products.

We can use many-to-many relationship here because products can be in many kits, and kits can be associated with many products.

Also, since the kits are just grouping the products, we can use self-joins. There are multiple ways we can implement self-joins.

Using has_and_belongs_to_many

You can read more about has_and_belongs_to_many on Rails docs.

Migration

class CreateJoinTableProductKits < ActiveRecord::Migration[6.0]
  def change
    create_table :product_kits, id: false do |t|
      t.references :product, null: false, foreign_key: true, index: false
      t.references :kit, null: false, foreign_key: { to_table: :products }, index: false
      t.index [:product_id, :kit_id], unique: true
    end
  end
end

Model

class Product < ApplicationRecord
  has_and_belongs_to_many :kits,
    join_table: :product_kits,
    class_name: 'Product',
    association_foreign_key: 'kit_id'
end

Using has_many through

This approach is better because later on in your project you can add more fields and validations in ProductKit model. As you know, our projects are always dynamic and most of the time(all the time) we end up modifying the flow. So, it is better to be prepared and use has_many :through from the beginning.

More on, has_many :through on Rails docs.

Migration

class CreateJoinTableProductKits < ActiveRecord::Migration[6.0]
  def change
    create_table :product_kits do |t|
      t.references :product, null: false, foreign_key: true, index: false
      t.references :kit, null: false, foreign_key: { to_table: :products }, index: false
      t.index [:product_id, :kit_id], unique: true

      t.timestamps
    end
  end
end

Model app/models/product.rb

class Product < ApplicationRecord
  has_many :product_kits, dependent: :destroy
  has_many :kits, through: :product_kits
end

Model app/models/product_kit.rb

class ProductKit < ApplicationRecord
  belongs_to :product
  belongs_to :kit, class_name: 'Product'
end