Spec-Zone.ru › Ruby on Rails 6.1

module ActiveRecord::Transactions::ClassMethods

Active Record Transactions

Транзакции — это защищенные блоки, где 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!

Публичные методы экземпляра

after_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 229
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]
after_create_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 241
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.

after_destroy_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 253
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.

after_rollback(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 261
def after_rollback(*args, &block)
  set_options_for_callbacks!(args)
  set_callback(:rollback, :after, *args, &block)
end

Этот обработчик вызывается после отката операции создания, обновления или удаления.

Обратитесь к документации after_commit для получения информации об опциях.

after_save_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 235
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 ].

after_update_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 247
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.

transaction(**options, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 208
def transaction(**options, &block)
  connection.transaction(**options, &block)
end

См. документацию API ConnectionAdapters::DatabaseStatements#transaction.

© 2004–2020 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API