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:
creates a session on a client that is used by the receiver of the
transaction
method call;starts a transaction on the session;
executes the given block;
commits the transaction if no exception raised in the block;
calls
after_commit
callbacks for all objects modified inside the transaction
aborts the transaction if an exception is raised in the block;
calls
after_rollback
callbacks for all objects modified inside the transaction
closes the session
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:
after transaction is committed if the object was modified inside the transaction;
after the object was persisted if the object was modified outside a transaction.
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.
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