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

    Каждый рубист, поработавший с 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+ моделей.

    Используйте гем и делитесь мнением. Любой вклад в дальнейшее развитие приветствуется!
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 5

      +1
      Ничего не написано про механизм работы и про требования к уровню изоляции транзакций в БД.
        0
        Для PostgreSQL есть citext и unique index, это 0 стороннего кода и все из коробки. Как и генерация хешей паролей с crypt и куча всего ещё, что на порядок более консистентнее, чем такой же код на ruby, который пытается угодить всем базам. Почему не доверить консистентность ПО, которое на этом специализируется?
          0

          Данный gem как раз использует родной unique index, и перехватывает ошибку от БД, трансформируя её в ошибку валидации, которую можно показать пользователю.


          Как пишет автор, в противном случае бы пользователь увидел страницу HTTP 500.

            0
            Не совсем 500, смотря как написано, достаточно перехватывать ActiveRecord::RecordNotUnique.
              0

              Можно перехватить, но информацию о том, какое поле вызвало такое исключение, придётся парсить из примерно вот такой строки:


              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 вы и не заметите.

        Only users with full accounts can post comments. Log in, please.