You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails中如何校验两列组合唯一,阻止反向敌对关系重复存入数据库

实现方案

你可以通过两种方案实现双向敌对关系的去重,分别对应不同的业务场景:

方案一:存储前统一ID顺序(推荐,易维护)

该方案会在数据存入前强制对齐两个队伍ID的顺序,原有唯一校验和索引无需做复杂修改即可生效:

  1. 修改模型代码,添加前置校验回调
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
  1. 数据库索引无需修改,原有联合唯一索引即可同时拦截正向、反向的重复记录。无论传入参数是team_id=10, rival_id=20还是team_id=20, rival_id=10,最终存入数据库的都是team_id=10, rival_id=20,自然会触发唯一约束。

方案二:自定义校验+表达式索引(适合需要保留ID原始顺序的场景)

如果业务需要明确区分敌对关系的主动/被动方向,不能修改ID存储顺序,可以用该方案:

  1. 模型层添加自定义双向重复校验
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
  1. 数据库层添加表达式唯一索引,避免应用层校验的竞态风险(以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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.13 13:24:04