Настоящая валидация на уникальность +12


Каждый рубист, поработавший с Ruby On Rails знаком с ORM ActiveRecord. Обсудим одну из предложенных из коробки валидаций, а именно, валидации на уникальность, и почему database_validations gem спасет консистенцию вашей базы данных.

Допустим, у вас есть модель пользователей с уникальностью на поле email, т.е.

class User < ApplicationRecord
  validates :email, uniqueness: true
end

Вы, возможно, уже знаете, что данная валидация выполняет следующий запрос

SELECT 1 FROM users WHERE email = $1

каждый раз, когда мы пытаемся сохранить запись в базу данных.

У данного подхода, есть несколько недостатков:

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

Во-вторых, данное решение не гарантирует уникальность из-за возможной гонки за данными. Несколько конкурентных операций могут одновременно узнать об отсутствии конкретной записи, в следствии чего, сохранить одни и те же данные.

Конечно, редкие случаи с гонкой данных возможно разрешить добавив ограничение на уникальность на уровне базы данных. Но в данном случае, вы не получите ошибку валидации, запрос к БД просто упадет и вся транзакция откатится.

В этой ситуации поможет gem database_validations, который предоставляет совместимость между ограничениями в базе данных и валидациями.

Основной смысл работы gem представлен в следующем коде:

def save(options = {})
  ActiveRecord::Base.connection.transaction(requires_new: true) { super }
rescue ActiveRecord::RecordNotUnique => e
  Helpers.handle_unique_error!(self, e)
  false
end


Таким образом, мы пробуем сохранить данные, если все другие валидации пройдены, если транзакция падает и откатывается, мы парсим ошибку и присваиваем правильные значения в errors нашего объекта.

Ознакомившись с документацией и бенчмарками, можно прийти к выводу, что данный gem ускорит процесс сохранения записей в базу данных минимум в два раза.

Благодаря поддержке таких баз данных, как PostgreSQL, SQLite, MySQL и обратной совместимости с validates_uniqueness_of, процесс замены на validates_db_uniqueness_of занимает считанные минуты.

Удобный matcher для RSpec также присутствует из коробки:

specify do
  expect(described_class)
    .to validate_db_uniqueness_of(:field)
    .with_message('duplicate')
    .with_where('(some_field IS NULL)')
    .scoped_to(:another_field)
    .with_index(:unique_index)
end

При переходе на новую валидацию, вам необходимо иметь ограничения на уникальность в базе данных, но если их еще нет, gem об этом укажет во время запуска приложения.

Гем протестирован на приложении с 100+ валидациями на уникальность среди 50+ моделей.

Используйте гем и делитесь мнением. Любой вклад в дальнейшее развитие приветствуется!




К сожалению, не доступен сервер mySQL