Проверка уникальности в базе данных, когда проверка имеет условие

Использование проверок уникальности в 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,

Кто-нибудь знает об "условном" индексе в базе данных PostgreSQL? Или другой способ исправить это?

1 ответ

Решение

Да, используйте частичный УНИКАЛЬНЫЙ индекс.

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

Пример:
Как добавить условный уникальный индекс на PostgreSQL

Я думаю, что - учитывая скорость - не ваша главная проблема - вы можете добиться правильной проверки уникальности без создания дополнительных индексов db. Цель может быть достигнута на прикладном уровне. Это особенно ценно, если вам нужна условная уникальность, поскольку некоторые базы данных (например, версии 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

Единственный недостаток, который я вижу, - это блокировка записей db для всего процесса проверки и сохранения. Это не сработает при большой нагрузке, но многие приложения в таких условиях все равно не работают.

Другие вопросы по тегам