Spec-Zone.ru › Ruby on Rails 8.1

модуль 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!

Общедоступные методы экземпляра

after_commit (*args, &block) Показать исходный код
# 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]
after_create_commit (*args, &block) Показать исходный код
# 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.

after_destroy_commit (*args, &block) Показать исходный код
# 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.

after_rollback (*args, &block) Показать исходный код
# 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.

after_save_commit (*args, &block) Показать исходный код
# 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 ].

after_update_commit (*args, &block) Показать исходный код
# 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.

current_transaction () Показать исходный код
# 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.

pool_transaction_isolation_level () Показать исходный код
# 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 уровень изоляции по умолчанию для пула подключений.

set_callback (name, *filter_list, &block) Показать исходный код
# 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.

Вызывает метод суперкласса
transaction (**options, &block) Показать исходный код
# 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.

with_pool_transaction_isolation_level (isolation_level) { || ... } Показать исходный код
# 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.

Spec-Zone.ru

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