Spec-Zone.ru › Ruby on Rails 4.2

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

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

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

Например:

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

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

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

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

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

Большинство баз данных не поддерживают настоящих вложенных транзакций. На момент написания этой документации единственной базой данных, о которой нам известно, что она поддерживает настоящие вложенные транзакции, является MS-SQL. По этой причине Active Record эмулирует вложенные транзакции с помощью контрольных точек (savepoints) в 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) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 237
def after_commit(*args, &block)
  set_options_for_callbacks!(args)
  set_callback(:commit, :after, *args, &block)
  unless ActiveRecord::Base.raise_in_transactional_callbacks
    ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
  end
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]

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

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

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

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

transaction(options = {}, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 218
def transaction(options = {}, &block)
  # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
  connection.transaction(options, &block)
end

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

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

Spec-Zone.ru

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