123456789_123456789_123456789_123456789_123456789_

Transactions

Version 4.0 of the MongoDB server introduces multi-document transactions. (Updates to multiple fields within a single document are atomic in all versions of MongoDB). Transactions require a non-standalone MongoDB topology and Ruby driver version 2.6 or higher. A higher level transaction API requires Mongoid version 9.0 or higher, while a lower level API requires Mongoid version 6.4 or higher.

Using Transactions

Higher Level API

A transaction can be started by calling the transaction method on an instance of a Mongoid document class, on a Mongoid document class, on or Mongoid module:

Band.transaction do
  Band.create(title: 'Led Zeppelin')
end

band = Band.create(title: 'Deep Purple')
band.transaction do
  band.active = false
  band.save!
end

Mongoid.transaction do
  band.destroy
end

When the transaction method is called, Mongoid does the following:

Note

Since a transaction is tied to a particular client, _only operations on the same client will be in scope of the transaction. Therefore it is recommended that only objects that use the same client are used inside the transaction method block.

class Author
  include Mongoid::Document
  store_in client: :encrypted_client
end

class User
  include Mongoid::Document
  store_in client: :encrypted_client
end

class Article
  include Mongoid::Document
  # This class uses the :default client
end

# Transaction is started on the :encrypted_client
Author.transaction do
  # This operation uses the same client, so it is in the transaction
  Author.create!
  # This operation also uses the same client, so it is in the transaction
  User.create!
  # This operation uses a different client, so it is NOT in the transaction
  Article.create!
end

Note

When transaction method is called on Mongoid module, the transaction is created using the :default client.

Aborting Transaction

Any exception raised inside the transaction method block aborts the transaction. Normally the raised exception passed on, except for the ::Mongoid::Errors::Rollback. This error should be raised if you want to explicitly abort the transaction without passing on an exception.

Callbacks

Transaction API introduces two new callbacks - after_commit and after_rollback.

after_commit callback is triggered for an object that was created, saved, or destroyed:

Note

In any case after_commit callback is triggered only after all other callbacks were executed successfully. Therefore, if the object is modified without a transaction, it is possible that the object was persisted, but after_commit callback was not triggered (for example, an exception raised in after_save callback).

after_rollback callback is triggered for an object that was created, saved, or destroyed inside a transaction if the transaction was aborted. after_rollback is never triggered without a transaction.

Lower Level API

In order to start a transaction, the application must have a session <sessions>.

A transaction can be started by calling the start_transaction method on a session, which can be obtained by calling the with_session method on either a model class or instance:

class Person
  include Mongoid::Document
end

Person.with_session do |session|
  session.start_transaction
end

person = Person.new
person.with_session do |session|
  session.start_transaction
end

It is also possible to specify read concern, write concern and read preference when starting a transaction:

Person.with_session do |session|
  session.start_transaction(
    read_concern: {level: :majority},
    write_concern: {w: 3},
    read: {mode: :primary})
end

A transaction may be committed or aborted. The corresponding methods to do so are commit_transaction and abort_transaction, again on the session instance:

Person.with_session do |session|
  session.commit_transaction
end

Person.with_session do |session|
  session.abort_transaction
end

If a session ends with an open transaction, the transaction is aborted.

The transaction commit can be retried if it fails. Here is the Ruby code to do so:

begin
  session.commit_transaction
rescue Mongo::Error => e
  if e.label?(Mongo::Error::UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
    retry
  else
    raise
  end
end

Note that in order to perform operations within the transaction, operations must use the same client that the session was initiated on. By default, all operations will be done on the default client:

class Person
  include Mongoid::Document
end

class Post
  include Mongoid::Document
end

Person.with_session do |s|
  s.start_transaction
  Person.create!
  Person.create!
  Post.create!
  s.commit_transaction
end

To explicitly use a different client, use the with method:

Post.with(client: :other) do
  Person.with(client: :other) do
    Person.with_session do |s|
      s.start_transaction
      Person.create!
      Person.create!
      Post.create!
      s.commit_transaction
    end
  end
end