Spec-Zone.ru › Ruby on Rails 4.1

модуль ActiveRecord::Transactions::ClassMethods

Active Record Транзакции

Транзакции — это защитные блоки, в которых операторы SQL считаются постоянными только в случае, если все они успешно выполнились как одно атомарное действие. Классический пример — перевод между двумя счетами, когда зачисление происходит только при успешном списании и наоборот. Транзакции обеспечивают целостность базы данных и защищают данные от ошибок программы или сбоев базы данных. Поэтому вы должны использовать блоки транзакций всякий раз, когда у вас есть несколько операторов, которые должны быть выполнены вместе или не выполняться вовсе.

Например:

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

В этом примере деньги будут взяты у Дэвида и переданы Мэри только в том случае, если ни withdrawal, ни deposit не вызовут исключение. Исключение заставит выполнить ОТКАТ, который вернет базу данных в состояние до начала транзакции. Однако имейте в виду, что данные объектов не будут возвращены в состояние до транзакции.

Разные классы Active Record в одной транзакции

Хотя метод транзакции класса вызывается для некоторого класса Active Record, объекты внутри блока транзакции не обязательно должны быть экземплярами этого класса. Это потому, что транзакции выполняются на уровне соединения с базой данных, а не на уровне модели.

В этом примере запись balance сохраняется с помощью транзакции, даже если transaction вызывается для класса Account:

Account.transaction do
  balance.save!
  account.save!
end

Метод transaction также доступен как метод экземпляра модели. Например, вы также можете сделать так:

balance.transaction do
  balance.save!
  account.save!
end

Транзакции не распределяются по соединениям с базой данных

Транзакция действует на одном соединении с базой данных. Если у вас есть несколько баз данных, специфичных для класса, транзакция не будет защищать взаимодействие между ними. Одним из решений является начало транзакции для каждого класса, чьи модели вы изменяете:

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 вызывается только после подтверждения обновления. См. ниже.

Обработка исключений и откат

Также имейте в виду, что исключения, сгенерированные в блоке транзакции, будут переданы (после запуска ОТКАТА), поэтому вы должны быть готовы перехватить их в своём коде приложения.

Исключение ActiveRecord::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)
  # => "PGError: 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 во вложенном блоке не вызывает ОТКАТ. Поскольку эти исключения обрабатываются в блоках транзакций, родительский блок этого не видит, и реальная транзакция подтверждается.

Чтобы получить ОТКАТ для вложенной транзакции, вы можете запросить реальную подтранзакцию, передав 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». Это работает в MySQL и PostgreSQL. SQLite3 версии ≥ '3.6.8' также поддерживает это.

Большинство баз данных не поддерживают истинные вложенные транзакции. На момент написания, единственная база данных, о которой нам известно, поддерживающая истинные вложенные транзакции, это MS-SQL. Из-за этого Active Record эмулирует вложенные транзакции, используя сохраняемые точки в MySQL и PostgreSQL. Подробнее о сохраняемых точках см. dev.mysql.com/doc/refman/5.6/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) Показать исходный код

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

Вы можете указать, что обратный вызов должен быть вызван только определенным действием с помощью опции :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]

Обратите внимание, что фикстуры транзакций не взаимодействуют должным образом с этой функцией. Используйте плагин test_after_commit для запуска этих хуков в тестах.

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

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

Проверьте документацию after_commit для опций.

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

См. ActiveRecord::Transactions::ClassMethods для подробной документации.

# File activerecord/lib/active_record/transactions.rb, line 206
def transaction(options = {}, &block)
  # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
  connection.transaction(options, &block)
end

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

Spec-Zone.ru

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