Rails中如何校验两列组合唯一,阻止反向敌对关系重复存入数据库
实现方案
你可以通过两种方案实现双向敌对关系的去重,分别对应不同的业务场景:
方案一:存储前统一ID顺序(推荐,易维护)
该方案会在数据存入前强制对齐两个队伍ID的顺序,原有唯一校验和索引无需做复杂修改即可生效:
- 修改模型代码,添加前置校验回调
class Rivalship < ApplicationRecord belongs_to :team belongs_to :rival, :class_name => "Team" # 保存前调整ID顺序,保证较小的ID始终存在team_id字段,较大ID存在rival_id字段 before_validation :normalize_team_pair_order # 原有唯一校验保持不变 validates_uniqueness_of :team_id, :scope => :rival_id # 额外添加校验,禁止队伍和自身成为敌对关系 validates :rival_id, comparison: { other_than: :team_id } private def normalize_team_pair_order return unless team_id.present? && rival_id.present? if team_id > rival_id # 交换两个ID的存储位置 self.team_id, self.rival_id = rival_id, self.team_id end end end
- 数据库索引无需修改,原有联合唯一索引即可同时拦截正向、反向的重复记录。无论传入参数是
team_id=10, rival_id=20还是team_id=20, rival_id=10,最终存入数据库的都是team_id=10, rival_id=20,自然会触发唯一约束。
方案二:自定义校验+表达式索引(适合需要保留ID原始顺序的场景)
如果业务需要明确区分敌对关系的主动/被动方向,不能修改ID存储顺序,可以用该方案:
- 模型层添加自定义双向重复校验
class Rivalship < ApplicationRecord belongs_to :team belongs_to :rival, :class_name => "Team" validates :rival_id, comparison: { other_than: :team_id } validate :check_bidirectional_duplicate private def check_bidirectional_duplicate return unless team_id.present? && rival_id.present? # 查询时排除当前记录自身,避免更新现有记录时误判 duplicate_exists = Rivalship.where.not(id: id) .where("(team_id = ? AND rival_id = ?) OR (team_id = ? AND rival_id = ?)", team_id, rival_id, rival_id, team_id) .exists? errors.add(:base, "该敌对关系组合已存在") if duplicate_exists end end
- 数据库层添加表达式唯一索引,避免应用层校验的竞态风险(以PostgreSQL为例):
class AddBidirectionalUniqueConstraintToRivalships < ActiveRecord::Migration[6.1] disable_ddl_transaction! # 使用concurrent创建索引必须关闭事务 def change # 移除原有普通联合唯一索引 remove_index :rivalships, [:rival_id, :team_id], if_exists: true # 基于最小、最大ID创建表达式唯一索引,无论ID顺序如何都会触发唯一约束 add_index :rivalships, "LEAST(team_id, rival_id), GREATEST(team_id, rival_id)", unique: true, algorithm: :concurrently, name: "idx_rivalships_unique_team_pair" end end
内容的提问来源于stack exchange,提问作者tommers
相关产品推荐
相关产品推荐

