Допустим, у вас есть модель пользователей с уникальностью на поле 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+ моделей.
Используйте гем и делитесь мнением. Любой вклад в дальнейшее развитие приветствуется!
Комментарии (5)
alexesDev
28.11.2018 00:49Для PostgreSQL есть citext и unique index, это 0 стороннего кода и все из коробки. Как и генерация хешей паролей с crypt и куча всего ещё, что на порядок более консистентнее, чем такой же код на ruby, который пытается угодить всем базам. Почему не доверить консистентность ПО, которое на этом специализируется?
philpirj
28.11.2018 01:00Данный gem как раз использует родной unique index, и перехватывает ошибку от БД, трансформируя её в ошибку валидации, которую можно показать пользователю.
Как пишет автор, в противном случае бы пользователь увидел страницу HTTP 500.
alexesDev
28.11.2018 01:07Не совсем 500, смотря как написано, достаточно перехватывать ActiveRecord::RecordNotUnique.
philpirj
28.11.2018 01:56Можно перехватить, но информацию о том, какое поле вызвало такое исключение, придётся парсить из примерно вот такой строки:
ERROR: duplicate key value violates unique constraint "index_users_on_email" DETAIL: Key (email)=(hello@example.com) already exists.
database_validations
делает это за вас, причём прозрачно, так, что разницы междуvalidates_db_uniqueness_of
иvalidates_uniqueness_of
вы и не заметите.
AEP
Ничего не написано про механизм работы и про требования к уровню изоляции транзакций в БД.