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 — это подходящее место для добавления подключения к очистке кэша, поскольку очистка его внутри транзакции может привести к повторной генерации кэша до обновления базы данных.
ПРИМЕЧАНИЕ: Callbacks дублируются по каждому коллбеку фильтром.
Попытка определить несколько коллбеков с одинаковым фильтром приведёт к выполнению только одного коллбека.
Например:
after_commit :do_something after_commit :do_something # only the last one will be called
Это относится ко всем вариантам коллбеков after_*_commit.
after_commit :do_something after_create_commit :do_something after_save_commit :do_something
Рекомендуется использовать параметр on: для указания, когда следует выполнить коллбек.
after_commit :do_something, on: [:create, :update]
Это эквивалентно использованию after_create_commit и after_update_commit, но не будет дублироваться.
Ограничения
Если вы используете MySQL, не используйте операторы языка определения данных (DDL) во вложенных блоках транзакций, которые эмулируются с помощью точек сохранения. То есть, не выполняйте операторы, такие как ‘CREATE TABLE’, внутри таких блоков. Это связано с тем, что MySQL автоматически высвобождает все точки сохранения при выполнении оператора DDL. Когда transaction завершается и пытается высвободить ранее созданную точку сохранения, возникает ошибка базы данных, потому что точка сохранения уже была автоматически высвобождена. Следующий пример демонстрирует проблему:
Model.lease_connection.transaction do # BEGIN
Model.lease_connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
Model.lease_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 266 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 278 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 290 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 298 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 272 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 284 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 245 def current_transaction connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION end
Возвращает представление текущего состояния транзакции, которое может быть верхней транзакцией, точкой сохранения или отсутствием транзакции.
Объект всегда возвращается, независимо от того, активна ли транзакция в данный момент. Для проверки того, была ли открыта транзакция, используйте current_transaction.open?.
См. документацию ActiveRecord::Transaction для подробного описания поведения.
# File activerecord/lib/active_record/transactions.rb, line 305
def set_callback(name, *filter_list, &block)
options = filter_list.extract_options!
filter_list << options
if name.in?([:commit, :rollback]) && options[:on]
fire_on = Array(options[:on])
assert_valid_transaction_action(fire_on)
options[:if] = [
-> { transaction_include_any_action?(fire_on) },
*options[:if]
]
end
super(name, *filter_list, &block)
end Аналогично ActiveSupport::Callbacks::ClassMethods#set_callback, но с поддержкой опций, доступных для обратных вызовов after_commit и after_rollback.
# File activerecord/lib/active_record/transactions.rb, line 232
def transaction(**options, &block)
with_connection do |connection|
connection.transaction(**options, &block)
end
end См. документацию API ConnectionAdapters::DatabaseStatements#transaction.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.