© Subhashini Chellappan and Dharanitharan Ganesan 2020
S. Chellappan, D. GanesanMongoDB Recipeshttps://doi.org/10.1007/978-1-4842-4891-1_6

6. Multidocument Transactions

Subhashini Chellappan1  and Dharanitharan Ganesan2
(1)
Bangalore, India
(2)
Krishnagiri, Tamil Nadu, India
 

In Chapter 5, we discussed replica sets and sharding in MongoDB. In this chapter, we are going to discuss multidocument transactions in MongoDB, a new feature introduced in MongoDB 4.0.

Multidocument Transactions in MongoDB

In MongoDB, a write operation on single document is atomic, even if the write operation modifies multiple embedded documents within a single document. When a single write operation (e.g., db.collection.updateMany()) modifies multiple documents, the modification of each document is atomic, but the operation as a whole is not.

Starting with version 4.0, MongoDB provides multidocument transactions for replica sets. Multidocument transactions help us to achieve all-or-nothing execution to maintain data integrity.

The multidocument transactions are atomic.
  • When a transaction commits, all data changes made in the transaction are saved and visible outside the transaction. The data changes are not visible outside the transaction until the transaction is committed.

  • When a transaction aborts, all data changes made in the transaction are discarded without ever becoming visible.

Note

Multidocument transactions are available only for a replica set. If we try to use multidocument transactions on a nonreplica set, we would get the error “Transaction numbers are only allowed on a replica set member or mongos,” as shown in Figure 6-1.

../images/475461_1_En_6_Chapter/475461_1_En_6_Fig1_HTML.jpg
Figure 6-1

Error on usage of multidocument transaction on a nonreplica set

Limitations of Transactions

Transactions do have some limitations, which are given here.
  • You can specify CRUD operations only on existing collections. The collections can be in different databases.

  • You cannot perform read/write operations on config, admin, and local databases.

  • You cannot write to system.* collections.

  • You cannot create or drop indexes inside a transaction.

  • You cannot perform non-CRUD operations inside a transaction.

  • You cannot return an operation’s query plan (i.e., explain).

Transactions and Sessions

In MongoDB, transactions are associated with sessions. MongoDB’s sessions provide a framework that supports consistency and writes that can be retried. MongoDB’s sessions are available only for replica sets and shared clusters. A session is required to start the transaction. You cannot run a transaction outside a session, and a session can run only one transaction at a time. A session is essentially a context.

There are three commands that important in working with transactions.
  • session.startTransaction(): To start a new transaction in the current session.

  • session.commitTransaction(): To save changes made by the operations in the transaction.

  • session.abortTransaction(): To abort the transaction without saving it.

Recipe 6-1. Working with Multidocument Transactions

In this recipe, we are going to discuss how to work with multidocument transactions .

Problem

You want to work with multidocument transactions.

Solution

Use session.startTransaction(), session.commitTransaction(), and session.abortTransaction().

How It Works

Let’s follow the steps in this section to work with multidocument transactions .

Step 1: Multidocument Transactions

To work with multidocument transactions, first we create an employee collection under the employee database as shown here.
use employee
db.createCollection("employee")
Here is the output,
myrs:PRIMARY> use employee
switched to db employee
myrs:PRIMARY> db.createCollection("employee")
{
        "ok" : 1,
        "operationTime" : Timestamp(1552385760, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1552385760, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
Next, insert a few documents as shown here.
db.employee.insert([{_id:1001, empName:"Subhashini"},{_id:1002, empName:"Shobana"}])
Here is the output,
myrs:PRIMARY> db.employee.insert([{_id:1001, empName:"Subhashini"},{_id:1002, empName:"Shobana"}])
BulkWriteResult({
        "writeErrors" : [ ],
        "writeConcernErrors" : [ ],
        "nInserted" : 2,
        "nUpserted" : 0,
        "nMatched" : 0,
        "nModified" : 0,
        "nRemoved" : 0,
        "upserted" : [ ]
})
Now, create a session as shown here.
session = db.getMongo().startSession()
Here is the output,
myrs:PRIMARY> session = db.getMongo().startSession()
session { "id" : UUID("55d56ef2-cab1-40c0-8d01-c2f75b5696b5") }
Next, start the transaction and insert a few documents as shown here.
session.startTransaction()
session.getDatabase("employee").employee.insert([{_id:1003,empName:"Taanushree"},{_id:1004, empName:"Aruna M S"}])
Here is the output,
myrs:PRIMARY> session.startTransaction()
myrs:PRIMARY> session.getDatabase("employee").employee.insert([{_id:1003,empName:"Taanushree"},{_id:1004, empName:"Aruna M S"}])
BulkWriteResult({
        "writeErrors" : [ ],
        "writeConcernErrors" : [ ],
        "nInserted" : 2,
        "nUpserted" : 0,
        "nMatched" : 0,
        "nModified" : 0,
        "nRemoved" : 0,
        "upserted" : [ ]
})
Now, we try to read the collection from inside and outside of the transactions . First, we will read the collection from inside the transaction.
session.getDatabase("employee").employee.find()
Here is the output,
myrs:PRIMARY> session.getDatabase("employee").employee.find()
{ "_id" : 1001, "empName" : "Subhashini" }
{ "_id" : 1002, "empName" : "Shobana" }
{ "_id" : 1003, "empName" : "Taanushree" }
{ "_id" : 1004, "empName" : "Aruna M S" }

We can see the modifications from inside the transaction.

Second, we will try to read the collection from outside of the transaction.
db.employee.find()
myrs:PRIMARY> db.employee.find()
{ "_id" : 1001, "empName" : "Subhashini" }
{ "_id" : 1002, "empName" : "Shobana" }

Because the transactions are not committed, we cannot see modifications outside the transaction .

Issue the following command to commit the transaction.
session.commitTransaction()
Here is the ouput:
myrs:PRIMARY> session.commitTransaction()
Now, we can see the modifications outside of the transaction also.
myrs:PRIMARY> db.employee.find()
{ "_id" : 1001, "empName" : "Subhashini" }
{ "_id" : 1002, "empName" : "Shobana" }
{ "_id" : 1003, "empName" : "Taanushree" }
{ "_id" : 1004, "empName" : "Aruna M S" }

Recipe 6-2. Isolation Test Between Two Concurrent Transactions

In this recipe, we are going to discuss how to perform an isolation test between two concurrent transactions.

Problem

You want to perform an isolation test between two concurrent transactions.

Solution

Use session.startTransaction(), session.commitTransaction() and session.abortTransaction().

How It Works

Let’s follow the steps in this section to perform an isolation test between two concurrent transactions.

Step 1: Isolation Test Between Two Concurrent Transactions

Create a first connection as shown here.
var session1 = db.getMongo().startSession()
session1.startTransaction()
session1.getDatabase("employee").employee.update({_id:1003},{$set:{designation: "TL" }})
Here is the output,
myrs:PRIMARY> var session1 = db.getMongo().startSession()
myrs:PRIMARY> session1.startTransaction()
myrs:PRIMARY> session1.getDatabase("employee").employee.update({_id:1003},{$set:{designation: "TL" }})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Next, read the collection as shown here.
session1.getDatabase("employee").employee.find()
Here is the output,
myrs:PRIMARY> session1.getDatabase("employee").employee.find()
{ "_id" : 1001, "empName" : "Subhashini" }
{ "_id" : 1002, "empName" : "Shobana" }
{ "_id" : 1003, "empName" : "Taanushree", "designation" : "TL" }
{ "_id" : 1004, "empName" : "Aruna M S" }
Now, create a second connection and update the documents as shown here.
var session2 = db.getMongo().startSession()
session2.startTransaction()
session2.getDatabase("employee").employee.update({_id:{$in:[1001,1004]}},{$set:{designation:"SE"}},{multi:"true"})
Here is the output,
myrs:PRIMARY> var session2 = db.getMongo().startSession()
myrs:PRIMARY> session2.startTransaction()
myrs:PRIMARY> session2.getDatabase("employee").employee.update({_id:{$in:[1001,1004]}},{$set:{designation:"SE"}},{multi:"true"})
WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 2 })
Next, read the collection as shown here.
session2.getDatabase("employee").employee.find()
Here is the output,
myrs:PRIMARY> session2.getDatabase("employee").employee.find()
{ "_id" : 1001, "empName" : "Subhashini", "designation" : "SE" }
{ "_id" : 1002, "empName" : "Shobana" }
{ "_id" : 1003, "empName" : "Taanushree" }
{ "_id" : 1004, "empName" : "Aruna M S", "designation" : "SE" }

Here, the transactions are isolated, and each transaction shows the modification that it has made itself.

Recipe 6-3. Transactions with Write Conflicts

In this recipe, we are going to discuss write conflicts with transactions.

Problem

You want to see the error message for write conflicts that occurs when two transactions try to modify the same document.

Solution

Use session.startTransaction(), session.commitTransaction() and session.abortTransaction().

How It Works

Let’s follow the steps in this section to manage write conflicts between two concurrent transactions.

Step 1: Transactions with Write Conflicts

Create a first connection as shown here.
var session1 = db.getMongo().startSession()
session1.startTransaction()
session1.getDatabase("employee").employee.update({empName:"Subhashini"},{$set:{empName: "Subha" }})
Here is the output,
myrs:PRIMARY> var session1 = db.getMongo().startSession()
myrs:PRIMARY> session1.startTransaction()
myrs:PRIMARY> session1.getDatabase("employee").employee.update({empName:"Subhashini"},{$set:{empName: "Subha" }})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
myrs:PRIMARY>
Now, create a second connection and try to update the same document as shown here.
var session2 = db.getMongo().startSession()
session2.startTransaction()
session2.getDatabase("employee").employee.update({empName:"Subhashini"},{$set:{empName: "Subha" }})
Here is the output,
myrs:PRIMARY> var session2 = db.getMongo().startSession()
myrs:PRIMARY> session2.startTransaction()
myrs:PRIMARY> session2.getDatabase("employee").employee.update({empName:"Subhashini"},{$set:{empName: "Subha" }})
WriteCommandError({
        "errorLabels" : [
                "TransientTransactionError"
        ],
        "operationTime" : Timestamp(1552405529, 1),
        "ok" : 0,
        "errmsg" : "WriteConflict",
        "code" : 112,
        "codeName" : "WriteConflict",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1552405529, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
})

Here, MongoDB detects a write conflict immediately, even though the transactions are not yet committed.

Recipe 6-4. Discarding Data Changes with abortTransaction

In this recipe, we are going to discuss how to discard data changes with abortTransaction.

Problem

You want to discard data changes with abortTransaction.

Solution

Use session.startTransaction() , session.commitTransaction() , and session.abortTransaction().

How It Works

Let’s follow the steps in this section to discard data changes using the abortTransaction method.

Step 1: Discard Data Changes

Create a student collection under the student database as shown here.
use student;
db.createCollection("student")
Here is the output,
myrs:PRIMARY> use student;
switched to db student
myrs:PRIMARY> db.createCollection("student")
{
        "ok" : 1,
        "operationTime" : Timestamp(1552406937, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1552406937, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
myrs:PRIMARY>
Next, insert a few documents as shown here.
db.student.insert({_id:1001,name:"subhashini"})
db.student.insert({_id:1002,name:"shobana"})
Start a transaction and insert a document.
var session1 = db.getMongo().startSession()
session1.startTransaction()
session1.getDatabase("student").student.insert({_id:1003,name:"Taanushree"})
Here is the output,
myrs:PRIMARY> var session1 = db.getMongo().startSession()
myrs:PRIMARY> session1.startTransaction()
myrs:PRIMARY> session1.getDatabase("student").student.insert({_id:1003,name:"Taanushree"})
WriteResult({ "nInserted" : 1 })
myrs:PRIMARY>
Now, find the document from the collection and session.
db.student.find()
Here is the output,
myrs:PRIMARY> db.student.find()
{ "_id" : 1001, "name" : "subhashini" }
{ "_id" : 1002, "name" : "shobana" }
myrs:PRIMARY>session1.getDatabase("student").student.find()
myrs:PRIMARY> session1.getDatabase("student").student.find()
{ "_id" : 1001, "name" : "subhashini" }
{ "_id" : 1002, "name" : "shobana" }
{ "_id" : 1003, "name" : "Taanushree" }
myrs:PRIMARY>
Now, you can discard the data changes with abortTransaction as shown here.
session1.abortTransaction()
Here is the output,
myrs:PRIMARY> session1.abortTransaction()
Now, you can find the collection as shown here.
db.student.find()
myrs:PRIMARY> db.student.find()
{ "_id" : 1001, "name" : "subhashini" }
{ "_id" : 1002, "name" : "shobana" }

Here, the data changes are discarded.

Note

Multidocument transactions are only available for deployments that use WiredTiger storage engine.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.109.234