带双作用域的唯一性验证未按预期工作问题排查
问题描述
我有一个GroupItem模型,配置了唯一性验证以确保同一记录仅能加入一个分组,但设置group_id和groupable_type为作用域后,创建不同分组下的同一关联记录时验证仍失败。具体代码及测试过程如下:
# A member of a Group class GroupItem < ApplicationRecord belongs_to :group belongs_to :groupable, polymorphic: true, optional: true # You can only be a member of a group once validates_uniqueness_of_without_deleted :groupable_id, uniqueness: { scope: %i[group_id groupable_type] } end
当创建两个独立分组并尝试将同一Client记录加入两个分组时,第二个GroupItem的验证失败,尽管两个记录的group_id不同:
client = Client.create group1 = Group.create group2 = Group.create group_item_1 = GroupItem.create(group: group1, groupable: client) group_item_2 = GroupItem.new(group: group2, groupable: client) > group_item_1 #<GroupItem:0x000000012ac86a60> { :id => 6, :groupable_type => "Client", :groupable_id => 7, :group_id => 7, :deleted_at => nil, :created_at => Fri, 07 Jun 2024 16:03:29.705078000 EDT -04:00, :updated_at => Fri, 07 Jun 2024 16:03:29.705078000 EDT -04:00 } > group_item_2 #<GroupItem:0x000000011f77db00> { :id => nil, :groupable_type => "Client", :groupable_id => 7, :group_id => 8, :deleted_at => nil, :created_at => nil, :updated_at => nil } > group_item_2.valid? false > group_item_2.errors #<ActiveModel::Errors:0x000000012aae5fd0 @base=#<GroupItem id: nil, groupable_type: "Client", groupable_id: 7, group_id: 8, deleted_at: nil, created_at: nil, updated_at: nil>, @errors=[#<ActiveModel::Error attribute=groupable_id, type=taken, options={:uniqueness=>{:scope=>[:group_id, :groupable_type]}, :value=>7}>]>
请问我的唯一性验证配置哪里出错了?
解决方案
你的问题出在validates_uniqueness_of_without_deleted的参数结构错误:你把scope选项嵌套在了uniqueness哈希中,但这个方法(通常来自paranoia这类软删除gem)的参数规则和Rails原生validates_uniqueness_of一致,需要将scope作为顶层选项直接传入。
修正后的模型代码:
class GroupItem < ApplicationRecord belongs_to :group belongs_to :groupable, polymorphic: true, optional: true # You can only be a member of a group once validates_uniqueness_of_without_deleted :groupable_id, scope: %i[group_id groupable_type] end
补充说明
仅靠模型验证无法完全避免并发场景下的竞态条件(比如两个请求同时创建同一分组下的同一关联记录),因此必须配合数据库唯一索引来保障数据一致性。创建对应的迁移文件:
# db/migrate/XXXXXX_add_unique_index_to_group_items.rb class AddUniqueIndexToGroupItems < ActiveRecord::Migration[7.0] def change add_index :group_items, [:groupable_id, :groupable_type, :group_id], unique: true, where: "deleted_at IS NULL" end end
这个索引会忽略已软删除的记录,同时确保同一groupable记录在同一分组下只能存在一条有效记录。
内容的提问来源于stack exchange,提问作者Jeremy Thomas
相关产品推荐
相关产品推荐

