ClientSession and Transactions¶
Overview¶
mongomock-ng supports ClientSession and transactions, allowing you to test transactional code that uses MongoDB sessions.
Basic Usage¶
Starting a Session¶
import mongomock
client = mongomock.MongoClient()
session = client.start_session()
# Use the session...
session.end_session()
Context Manager¶
Sessions support the context manager pattern, which automatically calls end_session():
Transactions¶
Starting a Transaction¶
session = client.start_session()
session.start_transaction()
try:
# Perform operations within the transaction
collection.insert_one({'_id': 1, 'value': 'test'}, session=session)
collection.update_one({'_id': 1}, {'$set': {'value': 'modified'}}, session=session)
# Commit the transaction
session.commit_transaction()
except Exception:
# Abort the transaction on error
session.abort_transaction()
raise
finally:
session.end_session()
Transaction Context Manager¶
Transactions support the context manager pattern:
with client.start_session() as session:
with session.start_transaction():
# Operations are automatically committed on clean exit
collection.insert_one({'_id': 1, 'value': 'test'}, session=session)
collection.update_one({'_id': 1}, {'$set': {'value': 'modified'}}, session=session)
# If an exception occurs, the transaction is automatically aborted
with_transaction() Helper¶
The with_transaction() method provides a convenient way to execute a callback within a transaction:
def callback(session):
collection.insert_one({'_id': 1, 'value': 'test'}, session=session)
collection.update_one({'_id': 1}, {'$set': {'value': 'modified'}}, session=session)
return 'success'
with client.start_session() as session:
result = session.with_transaction(callback)
print(result) # 'success'
Transaction Isolation¶
mongomock-ng uses a snapshot-based isolation mechanism for transactions:
- When a transaction starts, the first collection accessed is snapshotted
- Writes within the transaction modify the snapshot
- On
commit_transaction(), the snapshot replaces the original data - On
abort_transaction(), the original data is restored
Example: Abort Transaction¶
client = mongomock.MongoClient()
collection = client.db.collection
collection.insert_one({'_id': 1, 'value': 'original'})
session = client.start_session()
with session.start_transaction():
collection.update_one({'_id': 1}, {'$set': {'value': 'modified'}}, session=session)
session.abort_transaction()
# The update was aborted, original value is preserved
doc = collection.find_one({'_id': 1})
print(doc['value']) # 'original'
Example: Commit Transaction¶
client = mongomock.MongoClient()
collection = client.db.collection
collection.insert_one({'_id': 1, 'value': 'original'})
session = client.start_session()
with session.start_transaction():
collection.update_one({'_id': 1}, {'$set': {'value': 'modified'}}, session=session)
session.commit_transaction()
# The update was committed
doc = collection.find_one({'_id': 1})
print(doc['value']) # 'modified'
Supported Operations¶
The session parameter is supported in all CRUD operations:
insert_one(),insert_many()update_one(),update_many(),replace_one()delete_one(),delete_many()find_one(),find_one_and_update(),find_one_and_replace(),find_one_and_delete()find()(returns a cursor that respects the session)aggregate()count_documents(),distinct()bulk_write()create_index(),create_indexes(),drop_index(),drop_indexes()drop(),rename()
Database Operations¶
The session parameter is also supported in database operations:
list_collections(),list_collection_names()drop_collection()create_collection()dereference()
Session Properties¶
session = client.start_session()
# Check if session has ended
print(session.has_ended) # False
# Check if transaction is active
print(session.in_transaction) # False
# Get session ID
print(session.session_id) # {'id': UUID('...')}
# Get the client that created the session
print(session.client) # MongoClient instance
# Get session options
print(session.options) # SessionOptions instance
Client Isolation¶
Each MongoClient instance has isolated storage. Transactions on one client do not affect data on another client:
client1 = mongomock.MongoClient()
client2 = mongomock.MongoClient()
collection1 = client1.db.collection
collection2 = client2.db.collection
collection1.insert_one({'_id': 1, 'value': 'client1'})
# client2 doesn't see client1's data
print(collection2.find_one({'_id': 1})) # None
Error Handling¶
Starting a Transaction When One is Already Active¶
session = client.start_session()
session.start_transaction()
try:
session.start_transaction() # Raises InvalidOperation
except mongomock.InvalidOperation as e:
print(e) # "Transaction already in progress"
Committing/Aborting Without an Active Transaction¶
session = client.start_session()
try:
session.commit_transaction() # Raises InvalidOperation
except mongomock.InvalidOperation as e:
print(e) # "No transaction started"
try:
session.abort_transaction() # Raises InvalidOperation
except mongomock.InvalidOperation as e:
print(e) # "No transaction started"
Using an Ended Session¶
session = client.start_session()
session.end_session()
try:
session.start_transaction() # Raises InvalidOperation
except mongomock.InvalidOperation as e:
print(e) # "Cannot use a session that has ended"
Limitations¶
Snapshot Isolation vs Real MongoDB¶
mongomock-ng uses a simplified snapshot-based isolation mechanism. This differs from real MongoDB's multi-version concurrency control (MVCC):
- In mongomock-ng, the snapshot is taken on the first collection access within a transaction
- In real MongoDB, the snapshot is taken when the transaction starts
This means that in mongomock-ng, if you perform operations on multiple collections within a transaction, each collection is snapshotted independently when first accessed.
Concurrent Transactions¶
mongomock-ng is designed for single-threaded testing. Concurrent transactions on the same client are not supported and may lead to unexpected behavior.
Real MongoDB Comparison¶
When comparing mongomock-ng behavior with real MongoDB in tests, be aware that:
- Real MongoDB replica set returns
$clusterTimeandoperationTimein command responses - mongomock-ng does not return these fields by default
- Use
excluding()in test comparisons to ignore these metadata fields:
Testing with Real MongoDB¶
To test your code against both mongomock-ng and real MongoDB, use the comparison pattern:
import mongomock
import pymongo
def test_with_both_clients():
fake_client = mongomock.MongoClient()
real_client = pymongo.MongoClient('mongodb://localhost:27017/')
# Test with mongomock
with fake_client.start_session() as session:
with session.start_transaction():
fake_client.db.collection.insert_one({'_id': 1}, session=session)
# Test with real MongoDB (requires replica set for transactions)
with real_client.start_session() as session:
with session.start_transaction():
real_client.db.collection.insert_one({'_id': 1}, session=session)
# Compare results
fake_doc = fake_client.db.collection.find_one({'_id': 1})
real_doc = real_client.db.collection.find_one({'_id': 1})
assert fake_doc == real_doc