验证有条件时,数据库中的唯一性验证

时间:2014-07-08 19:27:45

标签: ruby-on-rails postgresql indexing unique-index

当有多个进程时,在Rails中使用唯一性验证是不安全的,除非在数据库上强制执行约束(在我的情况下是PostgreSQL数据库,所以请参阅http://robots.thoughtbot.com/the-perils-of-uniqueness-validations)。

就我而言,唯一性验证是有条件的:只有在模型中的另一个属性变为真时才应强制执行。所以我有

class Model < ActiveRecord::Base
  validates_uniqueness_of   :text, if: :is_published?

  def is_published?
    self.is_published
  end
end

因此模型有两个属性:is_published(布尔值)和text(文本属性)。如果text为真,则Model类型is_published的所有模型都应该是唯一的。

使用http://robots.thoughtbot.com/the-perils-of-uniqueness-validations中建议的唯一索引太过限制,因为无论is_published的值如何,它都会强制执行约束。

是否有人知道&#34;有条件的&#34; PostgreSQL数据库的索引?还是另一种解决方法?

2 个答案:

答案 0 :(得分:3)

是的,请使用partial UNIQUE index

CREATE UNIQUE INDEX tbl_txt_is_published_idx ON tbl (text) WHERE is_published;

例:
How to add a conditional unique index on PostgreSQL

答案 1 :(得分:0)

我认为-给定速度不是您的主要问题-您可以在不创建其他数据库索引的情况下实现适当的唯一性验证。可以在应用程序级别上实现目标。如果您想要条件唯一性,这尤其有价值,因为某些数据库(例如MySQL <8)不支持部分索引(或所谓的过滤索引)。

我的解决方案基于以下假设:

  • 唯一性检查(验证器)由Rails在与依赖它的保存/销毁操作相同的事务中运行。

这个假设似乎是正确的:https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

  

#save和#destroy都包装在一个事务中,以确保您在验证或回调中所做的任何操作都将在其受保护的情况下发生。

     

事务调用可以嵌套。默认情况下,这会使嵌套事务块中的所有数据库语句成为父事务的一部分。

具有可以使用悲观锁定(https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html)来独占锁定要评估的唯一性的记录的验证器。这将防止执行另一个同时运行的验证程序以及验证程序之后实际上发生的所有事情,直到交易结束时释放锁为止。这样可以确保验证-保存对的原子性和适当的唯一性强制执行。

在您的代码中,它看起来像这样:

class Model < ActiveRecord::Base
  validates :text, uniqueness: {
    conditions: ->{ lock.where(is_published: true) }
  }
end

我唯一看到的缺点是在整个validate-save过程中都锁定了数据库记录。在重负载下无法很好地工作,但随后许多应用程序无论如何都无法在这种条件下工作。