модуль ActiveRecord::Transactions::ClassMethods
Active Record Транзакции
Транзакции — это защищённые блоки, в которых SQL-запросы остаются постоянными только в случае успешного выполнения всех операций как единого атомарного действия. Классический пример — перевод между двумя счетами, когда зачисление средств возможно только при успешном списании и наоборот. Транзакции обеспечивают целостность базы данных и защищают данные от ошибок программы или сбоев базы данных. Поэтому в основном вам следует использовать блоки транзакций всякий раз, когда у вас есть несколько операторов, которые необходимо выполнить вместе или вовсе не выполнять.
Например:
ActiveRecord::Base.transaction do david.withdrawal(100) mary.deposit(100) end
В этом примере деньги будут сняты с Давида и перечислены Марии только в том случае, если ни withdrawal ни deposit не вызовут исключение. Исключение заставит выполнить ОТКАТ, вернув базу данных в состояние до начала транзакции. Однако имейте в виду, что данные объектов не будут возвращены в состояние до транзакции.
Разные классы 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 — единственный вызываемый, когда обновление подтверждено. См. ниже.
Обработка исключений и откат
Также имейте в виду, что исключения, выброшенные внутри блока транзакции, будут перехвачены (после запуска ОТКАТА), поэтому вам следует быть готовыми к их перехвату в вашем коде приложения.
Исключением является исключение ActiveRecord::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 во вложенном блоке не вызывает ОТКАТ. Поскольку эти исключения перехватываются в блоках транзакций, родительский блок не видит их, и фактическая транзакция подтверждается.
Чтобы получить ОТКАТ для вложенной транзакции, можно запросить реальную дочернюю транзакцию, передав 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 231 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 237 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 249 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 257 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 243 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 277
def raise_in_transactional_callbacks
ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.')
true
end # File activerecord/lib/active_record/transactions.rb, line 282
def raise_in_transactional_callbacks=(value)
ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.')
value
end # File activerecord/lib/active_record/transactions.rb, line 210
def transaction(options = {}, &block)
connection.transaction(options, &block)
end См. ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction API.
© 2004–2018 David Heinemeier Hansson
Licensed under the MIT License.