Spec-Zone.ru › Ruby on Rails 5.1

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

Активный Запись Транзакции

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

Например:

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

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

Разные классы 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

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

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

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

Для получения ОТКАТА вложенной транзакции можно запросить реальную подтранзакцию, передав 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.7/en/savepoint.html.

Колбеки

Существует два типа колбеков, связанных с подтверждением и откатом транзакций: after_commit и after_rollback.

Колбеки after_commit вызываются для каждой сохранённой или удалённой записи внутри транзакции сразу после подтверждения транзакции. Колбеки after_rollback вызываются для каждой сохранённой или удалённой записи внутри транзакции сразу после отката транзакции или контрольной точки.

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

Ограничения

Если вы используете MySQL, не используйте операции Data Definition Language (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 230
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 236
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 248
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 256
def after_rollback(*args, &block)
  set_options_for_callbacks!(args)
  set_callback(:rollback, :after, *args, &block)
end

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

Пожалуйста, обратитесь к документации для after_commit для опций.

after_update_commit(*args, &block) Показать исходный код
# File activerecord/lib/active_record/transactions.rb, line 242
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 209
def transaction(options = {}, &block)
  connection.transaction(options, &block)
end

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

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

Spec-Zone.ru

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