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
相关产品推荐
相关产品推荐

