railsの中間テーブルでデータの組み合わせを一意にする方法


はじめに

Twitterのように、1人のユーザーは複数のツイートにいいね!ができて、1つのツイートは複数のユーザーにいいね
!されるような多対多の関係をよく実装することはあると思います。
あるユーザーは1つのツイートに対して1回しかいいね!はしないので組み合わせは一意である必要があります。
今回は中間テーブルのデータの組み合わせを一意にする方法を説明します。

開発環境

Rails 6.0.3
Ruby 2.7.1
テスト: Rspec, FactoryBot, shoulda-matchers

1. テーブル

今回使用するテーブルは下記の3つです。

Userテーブル

id name email
1 ユーザー1 [email protected]
2 ユーザー2 [email protected]

Tweetテーブル

id content
1 tweet1
2 tweet2

Likeテーブル(中間テーブル)

id user_id tweet_id
1 1 2
2 1 3

多対多の関係

多対多の説明に関しては今回は省略します。

user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
end
tweet.rb
class Tweet < ApplicationRecord
  has_many_to :likes
end
like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet
end

2. 組み合わせを一意にする方法

そして今回のメインに実装です。
やることは2つです。

  1. migrationファイルにadd_indexでunique制約をつける (DBに制約をつける)
  2. 中間テーブルのmodelにvalidationを追加する (アプリ側でバリデーションを追加)

2.1 migrationファイルにadd_indexでunique制約をつける

add_index :likes, [:user_id, :tweet_id], unique: trueを中間テーブルのmigrationファイルに追加します。
追加した後はmigrateを忘れないように!

class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      t.references :user, null: false, foreign_key: true
      t.references :tweet, null: false, foreign_key: true

      t.timestamps
    end
    add_index :likes, [:user_id, :tweet_id], unique: true   #ここを追加
  end
end

2.2 中間テーブルのmodelにvalidationを追加する

validates :hotel_id, uniqueness: { scope: :staff_id }を中間テーブルのモデルに追加します。

like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet

  validates :user_id, uniqueness: { scope: :tweet_id }  #ここを追加
end

必要な実装は以上です。
コンソールで同一の組み合わせは一度しか登録できないことを確認してみてください。

3. 一意であることを確認するRspecテスト

参考までにテストもご紹介します。
Rspec, FactoryBot, shoulda-matchersの設定はできているものとします。

like_spec.rb
require 'rails_helper'

RSpec.describe Like, type: :model do
  let(:user) { create(:user) }
  let(:tweet) { create(:tweet) }
  before { create(:like, user: user, tweet: tweet) }

  it { should belong_to(:user) }  # この行と下の行で多対多の関係を確認
  it { should belong_to(:tweet) }
  it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:tweet_id) } # ここで一意であることを確認
end

おわりに

ちゃんと組み合わせを一意にできましたでしょうか?
今回のような実装はよく使われると思うので参考になれば幸いです。