Трябва да включите session
в опциите за всички операции за четене/запис, които са активни по време на транзакция. Само тогава те действително се прилагат към обхвата на транзакцията, където можете да ги върнете обратно.
Като малко по-пълен списък и просто като използвате по-класическия Order/OrderItems
моделиране, което би трябвало да е доста познато на повечето хора с известен опит в релационни транзакции:
const { Schema } = mongoose = require('mongoose');
// URI including the name of the replicaSet connecting to
const uri = 'mongodb://localhost:27017/trandemo?replicaSet=fresh';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const orderSchema = new Schema({
name: String
});
const orderItemsSchema = new Schema({
order: { type: Schema.Types.ObjectId, ref: 'Order' },
itemName: String,
price: Number
});
const Order = mongoose.model('Order', orderSchema);
const OrderItems = mongoose.model('OrderItems', orderItemsSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
let session = await conn.startSession();
session.startTransaction();
// Collections must exist in transactions
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.createCollection())
);
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
let items = await OrderItems.insertMany(
[
{ order: order._id, itemName: 'Cheese', price: 1 },
{ order: order._id, itemName: 'Bread', price: 2 },
{ order: order._id, itemName: 'Milk', price: 3 }
],
{ session }
);
// update an item
let result1 = await OrderItems.updateOne(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ session }
);
log(result1);
// commit
await session.commitTransaction();
// start another
session.startTransaction();
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
/*
* $lookup join - expect Milk to be price: 4
*
*/
let joined = await Order.aggregate([
{ '$match': { _id: order._id } },
{ '$lookup': {
'from': OrderItems.collection.name,
'foreignField': 'order',
'localField': '_id',
'as': 'orderitems'
}}
]);
log(joined);
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
Така че обикновено препоръчвам да извикате променливата session
с малки букви, тъй като това е името на ключа за обекта "опции", където се изисква за всички операции. Запазването на това в конвенцията с малки букви позволява да се използват и неща като присвояването на ES6 Object:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Също така документацията на мангустите за транзакциите е малко подвеждаща или поне може да бъде по-описателна. Това, което се нарича db
в примерите всъщност е екземплярът на Mongoose Connection, а не основният Db
или дори mongoose
глобален импорт, тъй като някои може да тълкуват погрешно това. Забележете в списъка и горния откъс, това е получено от mongoose.connect()
и трябва да се съхранява в кода ви като нещо, до което имате достъп от споделен импорт.
Алтернативно можете дори да вземете това в модулен код чрез mongoose.connection
собственост, по всяко време след е установена връзка. Това обикновено е безопасно в неща като манипулатори на сървърни маршрути и други подобни, тъй като ще има връзка с базата данни до момента, в който кодът бъде извикан.
Кодът също така демонстрира session
използване в различните методи на модела:
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Всички find()
базирани методи и update()
или insert()
и delete()
всички базирани методи имат окончателен "блок с опции", където се очакват този ключ и стойност на сесията. save()
Единственият аргумент на метода е този блок с опции. Това казва на MongoDB да приложи тези действия към текущата транзакция в тази сесия.
Почти по същия начин, преди да бъде извършена транзакция, всякакви заявки за find()
или подобни, които не посочват тази session
опция не вижда състоянието на данните, докато транзакцията е в ход. Състоянието на модифицираните данни е достъпно само за други операции, след като транзакцията приключи. Имайте предвид, че това има ефект върху записванията, както е описано в документацията.
Когато бъде издадено „прекратяване“:
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Всички операции върху активната транзакция се премахват от състоянието и не се прилагат. Като такива те не са видими за последващите операции. В примера тук стойността в документа се увеличава и ще покаже извлечена стойност от 5
на текущата сесия. След session.abortTransaction()
обаче предишното състояние на документа се връща. Обърнете внимание, че всеки глобален контекст, който не е четел данни в същата сесия, не вижда промяна в това състояние, освен ако не е ангажимент.
Това трябва да даде обща представа. Има повече сложност, която може да бъде добавена за справяне с различни нива на неуспех при запис и повторни опити, но това вече е обстойно обхванато в документация и много примери или може да се отговори на по-специфичен въпрос.
Изход
За справка, изходът от включената обява е показан тук:
Mongoose: orders.deleteMany({}, {})
Mongoose: orderitems.deleteMany({}, {})
Mongoose: orders.insertMany([ { _id: 5bf775986c7c1a61d12137dd, name: 'Bill', __v: 0 }, { _id: 5bf775986c7c1a61d12137de, name: 'Ted', __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orders.insertOne({ _id: ObjectId("5bf775986c7c1a61d12137df"), name: 'Fred', __v: 0 }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.insertMany([ { _id: 5bf775986c7c1a61d12137e0, order: 5bf775986c7c1a61d12137dd, itemName: 'Cheese', price: 1, __v: 0 }, { _id: 5bf775986c7c1a61d12137e1, order: 5bf775986c7c1a61d12137dd, itemName: 'Bread', price: 2, __v: 0 }, { _id: 5bf775986c7c1a61d12137e2, order: 5bf775986c7c1a61d12137dd, itemName: 'Milk', price: 3, __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.updateOne({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
{
"n": 1,
"nModified": 1,
"opTime": {
"ts": "6626894672394452998",
"t": 139
},
"electionId": "7fffffff000000000000008b",
"ok": 1,
"operationTime": "6626894672394452998",
"$clusterTime": {
"clusterTime": "6626894672394452998",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Mongoose: orderitems.findOneAndUpdate({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2"), upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf775986c7c1a61d12137e2",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Milk",
"price": 5,
"__v": 0
}
Mongoose: orders.aggregate([ { '$match': { _id: 5bf775986c7c1a61d12137dd } }, { '$lookup': { from: 'orderitems', foreignField: 'order', localField: '_id', as: 'orderitems' } } ], {})
[
{
"_id": "5bf775986c7c1a61d12137dd",
"name": "Bill",
"__v": 0,
"orderitems": [
{
"_id": "5bf775986c7c1a61d12137e0",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Cheese",
"price": 1,
"__v": 0
},
{
"_id": "5bf775986c7c1a61d12137e1",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Bread",
"price": 2,
"__v": 0
},
{
"_id": "5bf775986c7c1a61d12137e2",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Milk",
"price": 4,
"__v": 0
}
]
}
]