Spec-Zone.ru › Ruby on Rails 5.2

модуль ActiveRecord::Transactions::ClassMethods

Active Record Транзакции

Транзакции — это защищённые блоки, в которых SQL-запросы становятся постоянными только в том случае, если все они могут быть выполнены как одно атомное действие. Классический пример — перевод между двумя счетами, где депозит возможен только в том случае, если списание прошло успешно, и наоборот. Транзакции обеспечивают целостность базы данных и защищают данные от ошибок программы или сбоев базы данных. Так что в принципе вы должны использовать блоки транзакций всякий раз, когда у вас есть ряд заявок, которые должны быть выполнены вместе или вообще не выполняться.

Например:

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

Этот пример заберёт деньги у Дэвида и отдаст их Мэри только в том случае, если ни withdrawal, ни deposit не вызовут исключения. Исключение принудит к отмене транзакции (ROLLBACK), что вернёт базу данных в состояние до начала транзакции. Однако имейте в виду, что данные объектов не будут возвращены в состояние до транзакции.

Разные классы Active Record в одной транзакции

Хотя метод класса transaction вызывается для некоторого класса Active Record, объекты в блоке транзакции не обязательно должны быть экземплярами этого класса. Это связано с тем, что транзакции применяются к одному соединению с базой данных, а не к каждой модели.

В этом примере запись balance транзакционно сохраняется, даже если transaction вызывается для класса Account:

Account.transaction do
  balance.save!
  account.save!
end

Метод transaction также доступен как метод экземпляра модели. Например, вы также можете сделать так:

balance.transaction do
  balance.save!
  account.save!
end

Транзакции не распределяются по соединениям с базой данных

Транзакция работает с одним соединением с базой данных. Если у вас есть несколько специализированных баз данных для разных классов, транзакция не защитит взаимодействие между ними. Одним из решений является запуск транзакции для каждого класса, модели которого вы изменяете:

Student.transaction do
  Course.transaction do
    course.enroll(student)
    student.units += course.units
  end
end

Это нелучшее решение, но полностью распределённые транзакции выходят за рамки Active Record.

save и destroy автоматически обернуты в транзакцию

И #save, и #destroy обернуты в транзакцию, гарантирующую, что всё, что вы делаете в валидации или обратных вызовах, будет выполняться под её защитой. Поэтому вы можете использовать валидацию для проверки значений, от которых зависит транзакция, или вызывать исключения в обратных вызовах для отката, включая after_* обратные вызовы.

Вследствие этого изменения в базе данных не видны вне вашего соединения до тех пор, пока операция не будет завершена. Например, если вы пытаетесь обновить индекс поисковой системы в after_save, индексатор не увидит обновлённую запись. Обратный вызов after_commit — единственный, который срабатывает после того, как обновление будет применено. Смотрите ниже.

Обработка исключений и откат

Также имейте в виду, что исключения, сгенерированные в блоке транзакции, будут распространяться (после вызова ROLLBACK), поэтому вы должны быть готовы их перехватывать в своём коде приложения.

Исключение ActiveRecord::Rollback — исключение, которое вызовет ROLLBACK при возникновении, но не будет повторно сгенерировано блоком транзакции.

Предупреждение: не следует перехватывать исключения ActiveRecord::StatementInvalid внутри блока транзакции. Исключения ActiveRecord::StatementInvalid указывают на то, что произошла ошибка на уровне базы данных, например, при нарушении уникального ограничения. В некоторых системах баз данных, таких как PostgreSQL, ошибки базы данных внутри транзакции делают всю транзакцию непригодной для использования до тех пор, пока она не будет перезапущена с начала. Вот пример, который демонстрирует проблему:

# Suppose that we have a Number model with a unique column called 'i'.
Number.transaction do
  Number.create(i: 0)
  begin
    # This will raise a unique constraint error...
    Number.create(i: 0)
  rescue ActiveRecord::StatementInvalid
    # ...which we ignore.
  end

  # On PostgreSQL, the transaction is now unusable. The following
  # statement will cause a PostgreSQL error, even though the unique
  # constraint is no longer violated:
  Number.create(i: 1)
  # => "PG::Error: ERROR:  current transaction is aborted, commands
  #     ignored until end of transaction block"
end

Следует перезапустить всю транзакцию, если возникло ActiveRecord::StatementInvalid.

Вложенные транзакции

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

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

создаёт и «Kotori», и «Nemu». Причина в том, что исключение ActiveRecord::Rollback во вложенном блоке не вызывает ROLLBACK. Поскольку эти исключения перехватываются в блоках транзакции, родительский блок этого не видит, и транзакция действительно применяется.

Для того, чтобы получить ROLLBACK для вложенной транзакции, можно запросить реальную подтранзакцию, передав requires_new: true. Если что-то пойдёт не так, база данных откатится до начала подтранзакции, не откатывая родительскую транзакцию. Если добавить это к предыдущему примеру:

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

создаётся только «Kotori». Это работает в MySQL и PostgreSQL. SQLite3 версии >= '3.6.8' также поддерживает это.

Большинство баз данных не поддерживают истинно вложенные транзакции. На момент написания, единственная база данных, о которой нам известно, поддерживающая истинно вложенные транзакции, это MS-SQL. По этой причине Active Record эмулирует вложенные транзакции, используя сохраняемые точки в MySQL и PostgreSQL. Для получения дополнительной информации о сохраняемых точках см. dev.mysql.com/doc/refman/5.7/en/savepoint.html.

Обратные вызовы

Существует два типа обратных вызовов, связанных с подтверждением и откатом транзакций: after_commit и after_rollback.

Обратные вызовы after_commit вызываются для каждой записи, сохранённой или удалённой в рамках транзакции, сразу после подтверждения транзакции. Обратные вызовы after_rollback вызываются для каждой записи, сохранённой или удалённой в рамках транзакции, сразу после отката транзакции или сохраняемой точки.

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

Ограничения

Если вы используете MySQL, не используйте операции языка определения данных (DDL) во вложенных блоках транзакций, эмулируемых сохраняемыми точками. То есть не выполняйте запросы вроде 'CREATE TABLE' в таких блоках. Это связано с тем, что MySQL автоматически освобождает все сохраняемые точки при выполнении операции DDL. Когда transaction завершается и пытается освободить созданную ранее сохраняемую точку, произойдёт ошибка базы данных, поскольку сохраняемая точка уже была автоматически освобождена. Следующий пример демонстрирует проблему:

Model.connection.transaction do                           # BEGIN
  Model.connection.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    Model.connection.create_table(...)                    # active_record_1 now automatically released
  end                                                     # RELEASE SAVEPOINT active_record_1
                                                          # ^^^^ BOOM! database error!
end

Обратите внимание, что «TRUNCATE» также является операцией DDL в MySQL!

Открытые методы экземпляра

after_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 232
def after_commit(*args, &block)
  set_options_for_callbacks!(args)
  set_callback(:commit, :after, *args, &block)
end

Этот обратный вызов вызывается после создания, обновления или удаления записи.

Вы можете указать, что обратный вызов должен срабатывать только для определённого действия с помощью опции :on:

after_commit :do_foo, on: :create
after_commit :do_bar, on: :update
after_commit :do_baz, on: :destroy

after_commit :do_foo_bar, on: [:create, :update]
after_commit :do_bar_baz, on: [:update, :destroy]
after_create_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 238
def after_create_commit(*args, &block)
  set_options_for_callbacks!(args, on: :create)
  set_callback(:commit, :after, *args, &block)
end

Сокращение для after_commit :hook, on: :create.

after_destroy_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 250
def after_destroy_commit(*args, &block)
  set_options_for_callbacks!(args, on: :destroy)
  set_callback(:commit, :after, *args, &block)
end

Сокращение для after_commit :hook, on: :destroy.

after_rollback(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 258
def after_rollback(*args, &block)
  set_options_for_callbacks!(args)
  set_callback(:rollback, :after, *args, &block)
end

Этот обратный вызов вызывается после отката создания, обновления или удаления.

Пожалуйста, проверьте документацию для after_commit для опций.

after_update_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 244
def after_update_commit(*args, &block)
  set_options_for_callbacks!(args, on: :update)
  set_callback(:commit, :after, *args, &block)
end

Сокращение для after_commit :hook, on: :update.

transaction(options = {}, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 211
def transaction(options = {}, &block)
  connection.transaction(options, &block)
end

См. документацию API ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction.

© 2004–2018 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API