Rails 使用 Uniqueness 验证注意事项

在 Rails 应用中使用 uniqueness validations 比较常见,用以校验字段唯一性。当数据库记录出现重复时,这种校验提供了很好的用户体验,但是单纯地在 Rails 应用层做校验还是不会确保数据完整性

会出现什么问题呢

首先开始看一个简单的 User 类

class User
  validates :email, presence: true, uniqueness: true
end

当你开始实例化一个用户对象时,Rails 就会通过给定的 email,使用执行 SELECT 语句查询数据库中是否存在该记录。假设校验通过,Rails 就会执行 INSERT 语句把实例化的用户对象持久化到数据库。在开发环境或者使用了单进程单实例的 web 服务器的生产环境中不会出现任何问题。
但是在生产环境中 WEBrick 跑一个实例很少见。多连接,高并发多进程且一个 Rails 应用部署在多台机器为常态。在例子中,当同一个 email 被两个进程同时处理时会发生什么?

Rails 使用 Uniqueness 验证注意事项_第1张图片
unique_without_index

原意想校验 email 唯一性,但相同的 email 被不同的进程同时处理时,都会成功保存在数据库中。所以,校验不仅仅要发生在 Rails 应用层,在数据库层也要校验。

数据库唯一性索引

Rails 给数据库建唯一性索引有两种方式

  • 添加迁移时
class AddEmailIndexToUser
  def change
    # If you already have non-unique index on email, you will need
    # to remove it before you're able to add the unique index.
    add_index :users, :email, nul: false, unique: true
  end
end
  • 添加 model 时
rails generate model user email:string:uniq
  • migration 新建数据库表时
class CreateFoos < ActiveRecord::Migration
    def change
      create_table :users do |t|
        t.string :email, :null => false
        t.index :email, unique: true
      end
    end
end

有了数据库唯一性索引之后,再次重现上述两个进程同时处理插入相同 email 操作

Rails 使用 Uniqueness 验证注意事项_第2张图片
unique_with_index

唯一性索引为数据库拒绝插入相同字段的记录,第二次尝试插入的记录会抛出 ActiveRecord::RecordNotUnique 异常。此时应用持续内部报错,为了提供更好的用户体验,你需要在处理该请求的 action 处理这个异常,或者在 class level 使用 rescue_from
在 Application_controller 中添加

rescue_from ActiveRecord::RecordNotUnique do |exception|
  render_404
end

数据库唯一性索引还能干嘛

对于 has_one 关系,只是表明了起只处理一个对象而不是一个集合,本身并不确保数据完整性。
例如,在一个 Rails 应用中有一个 Profile model, 一个用户只有一个 profile 对象。

class User
  has_one :profile
  validates :email, presence: true, uniqueness: true
end

class Profile
  belongs_to :user
end

我们期望 profiles表中的记录,user_id的值是唯一的,不能重复,但是 has_one做不到。所以我们需要在 profiles 表中给 user_id 字段添加唯一性索引。

如何在 Rails 应用中找出缺失的唯一性索引

索引有利有弊,利于查询弊于更新且占用存储空间。但是在一些必要的情况下,我们如何快速监测出那里缺失了建立索引呢?
你可以全局搜索项目中的 validates_uniqueness_of, uniqueness, uniqueness 关键字,这些地方对应的字段都需要在数据库中建立唯一性索引。或者,你可以使用 Consistency Fail,这个工具帮助监测 Rails 应用中缺失的唯一性索引,其更多使用请查看 README
使用 consistency_fail一个可知的缺点就是其不能识别 has_one的多态。
例如

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_one :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_one :pictures, as: :imageable
end

正常来说应建唯一索引:

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :imageable_id
      t.string  :imageable_type
      t.timestamps
    end

    add_index :pictures, [:imageable_type, :imageable_id]
  end
end

但是 consistency_fail 的提示却只有

add_index :pictures, [:imageable_id]

另外,如果迁移数据库的多态是这样写的话

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

则不要担心唯一性索引没有imageable_type的问题。

你可能感兴趣的:(Rails 使用 Uniqueness 验证注意事项)