1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails scopeを使った条件付きユニークバリデーションの書き方

1
Posted at

背景

上司からのコードレビューで理解するのに時間がかかった部分があったので記録する

指摘された内容

class BlockedStaff < ApplicationRecord
  belongs_to :user
  belongs_to :staff
  
  validates :user_id, uniqueness: { scope: :staff_id } # 指摘された部分
end

これは、ユーザーがブロックしているスタッフを管理しているモデルです
ユーザーは同じスタッフをブロックできないように、ユーザーとスタッフでユニーク制約がかけられています

指摘されたのはこの部分

validates :user_id, uniqueness: { scope: :staff_id }

意味合いとしてはvalidates :staff_id, uniqueness: { scope: :user_id }じゃない?

このようなレビューをいただきました

レビューについて

機能としてはどちらで記述しても、ユニーク制約のバリデーションはかかります

しかしレビュー通り意味は違ってきます

validates :user_id, uniqueness: { scope: :staff_id }の意味

  • user_idに対してバリデーションがかかっている
  • scope: :staff_idなので、「staff_idが同じレコード」が対象です

「staff_idが同じレコード群の中で、user_idが重複していないか」をチェックしている

validates :staff_id, uniqueness: { scope: :user_id }の意味

  • staff_idに対してバリデーションがかかっている
  • scope: :user_idなので、「user_idが同じレコード」が対象

「user_idが同じレコード群の中で、staff_idが重複していないか」をチェックしている

なぜvalidates :user_id, uniqueness: { scope: :staff_id }が間違っているのか?

class BlockedStaff < ApplicationRecord
  belongs_to :user
  belongs_to :staff
  
  validates :user_id, uniqueness: { scope: :staff_id } # 指摘された部分
end

BlockedStaffは、ユーザーがブロックしたスタッフを管理するモデルです、なので操作の主体はユーザーです

なのでvalidates :user_id, uniqueness: { scope: :staff_id }だと下記のイメージ

  1. staff_idが同じレコードに絞る
  2. その中でuser_idが重複していないかをチェックする

つまり、ユーザーが行う操作にも関わらずstaff_idの視点でバリデーションを行っています

逆にvalidates :staff_id, uniqueness: { scope: :user_id }の場合は

  1. user_idが同じレコードに絞る
  2. その中でstaff_idが重複していないかをチェックする

これはuser_idの視点でバリデーションが行われています

なので正しくは

class BlockedStaff < ApplicationRecord
  belongs_to :user
  belongs_to :staff
  
  validates :staff_id, uniqueness: { scope: :user_id } # 修正
end

具体的に何が変わるの?

意味合いだけでなく、実際の挙動にも違いがあります

validatesは、第一引数に指定したカラムに対してエラーメッセージを付与します

# validates :user_id の場合
blocked_staff.errors[:user_id]
# => ["has already been taken"]

# validates :staff_id の場合
blocked_staff.errors[:staff_id]
# => ["has already been taken"]

つまり、どちらのカラムにバリデーションをかけるかによって、エラーメッセージが付くカラムが変わります

BlockedStaffはユーザーがスタッフをブロックする操作なので、バリデーションエラーは「ブロックしようとしたスタッフ(staff_id)」に対して出る方が自然です

validates :user_idだと「ユーザーIDが重複しています」というエラーになり、操作しているユーザーからすると意味が伝わりにくくなります

感想

  • ん?となりながら理解できたことを言語化できたので良かったです
  • 可読性の高いコードを書く重要性が学べた

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?