rails で複合項目で一意性チェックを行う方法を備忘録として残しておく

環境

  • ruby: 3.0.5
  • rails: 6.1.7

実際のコード

[ER 図]

ER 図
ER 図

[model]

uniqueness + scope で validate を行う

class Book
  has_many :book_categories, dependent: :destroy
  has_many :categories, through: :book_categories
end

class BookCategory
  belongs_to :book
  belongs_to :category

  # book_id + category_id で一意かどうかをチェック
  validates :book_id, uniqueness: { scope: :category_id }
end

class Category
  has_many :book_categories
  has_many :books, through: :book_categories
end

[migrate]

unique index を追加する

class CreateBookCategoriess < ActiveRecord::Migration[6.1]
  def change
    create_table :book_categories do |t|
      t.references :book, foreign_key: true
      t.references :category, foreign_key: true

      t.timestamps
    end

    add_index :book_categories, [:book_id, :category_id], unique: true
    change_table_comment :book_categories, '本のカテゴリー'
  end
end

[rspec]

FactoryBot + shoulda-matchers 導入している場合

require 'rails_helper'

describe BookCategory, type: :model do
  describe 'validations' do
    subject { FactoryBot.build(:book_category) }

    context 'book_id' do
      it { is_expected.to validate_uniqueness_of(:book_id).scoped_to(:category_id) }
    end
  end
end