модуль ActiveRecord::Transactions::ClassMethods
Транзакции Active Record
Транзакции — это защитные блоки, в которых SQL-инструкции становятся постоянными только в том случае, если все они могут выполниться как единое атомарное действие. Классический пример — перевод между двумя счетами: зачисление возможно только в случае успешного списания, и наоборот. Транзакции обеспечивают целостность базы данных и защищают данные от ошибок программы и сбоев базы данных. Поэтому, в общем случае, блоки транзакций следует использовать всякий раз, когда необходимо выполнить вместе несколько инструкций — либо не выполнять их вовсе.
Например:
ActiveRecord::Base.transaction do david.withdrawal(100) mary.deposit(100) end
В этом примере деньги будут списаны со счета David и зачислены на счет Mary, только если ни 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.transaction do # BEGIN
Model.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
end # ^^^^ BOOM! database error! Обратите внимание, что «TRUNCATE» также является инструкцией DDL в MySQL!
Общедоступные методы экземпляра
# File activerecord/lib/active_record/transactions.rb, line 285 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 297 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 309 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 317 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 291 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 303 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 264 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 253 def pool_transaction_isolation_level connection_pool.pool_transaction_isolation_level end
Возвращает установленный ранее методом with_pool_transaction_isolation_level уровень изоляции по умолчанию для пула подключений.
# File activerecord/lib/active_record/transactions.rb, line 324
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 231
def transaction(**options, &block)
with_connection do |connection|
connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
connection.transaction(**options, &block)
end
end
end См. документацию API ConnectionAdapters::DatabaseStatements#transaction.
# File activerecord/lib/active_record/transactions.rb, line 240
def with_pool_transaction_isolation_level(isolation_level, &block)
if current_transaction.open?
raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
end
old_level = connection_pool.pool_transaction_isolation_level
connection_pool.pool_transaction_isolation_level = isolation_level
yield
ensure
connection_pool.pool_transaction_isolation_level = old_level
end Устанавливает указанный внутри блока уровень изоляции для всех транзакций текущего пула.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.