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
- calls
- aborts the transaction if an exception is raised in the block;
- calls
after_rollback
callbacks for all objects modified inside the transaction
- calls
- closes the session
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
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.
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