Hacking Limbo

Reading / Coding / Hacking

关于 ActiveRecord 的唯一性检查

在 Rails 中如果要验证数据字段值的唯一性通常都会用到 validates_uniqueness_of 方法,比如我要确保用户注册时填写的 Email 地址是唯一的,就会这样写:

class User < ActiveRecord::Base
  validates_uniqueness_of :email
end

如果了解 validates_uniqueness_of 的实现细节的话就会知道,单纯这样写无法完全保证 Email 值的唯一性,因为 ActiveRecord::Validations::UniquenessValidator 的执行与随后写入数据到数据库这两个操作之间存在时间差,在多用户并发访问的情况下就很容易写入重复的值。解决方法就是给 email 字段建一个 unique 的索引,把唯一性检查交给数据库来做,这样如果插入重复值而 validates_uniqueness_of 又没有检查到的话,就会抛 ActiveRecord::RecordNotUnique 异常。

其实这一点在 Rails 官方文档Concurrency and integrity 一节中有详细的讲解,我要说的是另一个问题,那就是既然 validates_uniqueness_of 不可靠,为什么不省掉这一步,直接捕捉(可能会抛出的)ActiveRecord::RecordNotUnique 异常?

前几天在 Ruby China 问了这个问题(链接),根据大家的回复总结出的答案就是“方便 form validation error messages 的显示”,因为 ActiveRecord::RecordNotUnique 这个异常没有包含太多信息(比如到底是哪个字段重复了),无法给予用户必要的提示。

所以结论是 validates_uniqueness_of 要写,而 ActiveRecord::RecordNotUnique 这个异常也要在 controller 里面捕捉并适当处理,比如这样(没有实际试验过):

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotUnique, with: :recheck_uniqueness

  private
    def recheck_uniqueness
      # trigger validations again so that duplication can be detected
      @user.valid?
    end
end