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
Transactions не распределяются по базам данных
Транзакция действует на одном соединении с базой данных. Если у вас несколько баз данных, специфичных для класса, транзакция не будет защищать взаимодействие между ними. Одним из решений является начало транзакции для каждого класса, модели которого вы изменяете:
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».
Большинство баз данных не поддерживают истинные вложенные транзакции. На момент написания статьи, единственной базой данных, о которой нам известно, поддерживающей истинные вложенные транзакции, является MS-SQL. Из-за этого Active Record эмулирует вложенные транзакции, используя сохраняемые точки. Смотрите dev.mysql.com/doc/refman/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, prepend_option) 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, **prepend_option) 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, **prepend_option) 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, prepend_option) 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 ], **prepend_option) 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, **prepend_option) 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
См. ConnectionAdapters::DatabaseStatements#transaction в документации по API.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.