module 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!
Методы публичного экземпляра
# 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]
# File activerecord/lib/active_record/transactions.rb, line 244 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.
# File activerecord/lib/active_record/transactions.rb, line 256 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.
# File activerecord/lib/active_record/transactions.rb, line 264 def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) end
Этот обработчик вызывается после отката операций создания, обновления или удаления.
Пожалуйста, ознакомьтесь с документацией after_commit для получения информации об опциях.
# File activerecord/lib/active_record/transactions.rb, line 238 def after_save_commit(*args, &block) set_options_for_callbacks!(args, on: [ :create, :update ]) set_callback(:commit, :after, *args, &block) end
Сокращение для after_commit :hook, on: [ :create, :update ].
# File activerecord/lib/active_record/transactions.rb, line 250 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.
# File activerecord/lib/active_record/transactions.rb, line 211
def transaction(options = {}, &block)
connection.transaction(options, &block)
end См. документацию API ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction.
© 2004–2019 David Heinemeier Hansson
Licensed under the MIT License.