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

Rails ActiveRecord唯一性验证抛错但记录仍被创建问题

解决Rails并发场景下唯一性验证失效的问题

问题背景

使用Ruby 3.0.4 + Rails 6.1.7.2,在Item模型中定义了如下唯一性验证:

# 模型 - item.rb
validates :name, uniqueness: { case_sensitive: false, scope: :user_id }

控制器创建逻辑:

# 控制器 - items.rb
def create
  item = @user.items.new(params)
  item.save!
rescue ActiveRecord::RecordInvalid => exception
  # 错误处理逻辑
end

非并发场景下验证正常,但同一用户发送多个并行请求时,会抛出ActiveRecord::RecordInvalid异常,仍有重复记录被创建(如8次并行请求中,1次成功创建,7次抛异常,但最终出现3条重复记录)。

问题根源

Rails模型层的唯一性验证是非原子操作:先查询数据库判断记录是否存在,再执行插入。并发场景下,多个请求会同时通过查询阶段的验证(此时数据库还未写入记录),随后同时插入,导致重复数据。模型验证无法拦截这种并发冲突。

解决方案

要彻底解决问题,必须结合模型验证和数据库唯一约束,两者缺一不可:

1. 添加数据库层面的唯一约束(核心)

数据库的唯一约束是原子性的,能从底层阻止重复数据插入。根据数据库类型创建对应索引:

针对PostgreSQL

生成迁移文件:

rails generate migration AddUniqueIndexToItemsNameAndUserId

编辑迁移文件,创建不区分大小写的联合唯一索引:

class AddUniqueIndexToItemsNameAndUserId < ActiveRecord::Migration[6.1]
  def change
    add_index :items, ["lower(name)", :user_id], unique: true, name: "index_items_on_lower_name_and_user_id"
  end
end

运行迁移:

rails db:migrate
针对MySQL

确保name列的collation设置为不区分大小写(如utf8mb4_general_ci),然后创建联合唯一索引:

class AddUniqueIndexToItemsNameAndUserId < ActiveRecord::Migration[6.1]
  def change
    add_index :items, [:name, :user_id], unique: true
  end
end

2. 处理数据库层面的异常

添加约束后,并发插入会抛出ActiveRecord::RecordNotUnique异常,需要在控制器中捕获并统一错误提示:

# 控制器 - items.rb
def create
  item = @user.items.new(params)
  item.save!
rescue ActiveRecord::RecordInvalid => exception
  render json: { error: exception.message }, status: :unprocessable_entity
rescue ActiveRecord::RecordNotUnique => exception
  # 返回与模型验证一致的错误提示
  render json: { error: "Name must be unique for a user." }, status: :unprocessable_entity
end

说明

  • 模型层验证仍需保留:用于非并发场景下提前给用户反馈,减少不必要的数据库请求。
  • 数据库约束是最后防线:确保无论并发程度如何,都不会产生重复记录。

内容的提问来源于stack exchange,提问作者Pranjal Nanavaty

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 16:07:03