Du måste inkludera session
inom alternativen för alla läs-/skrivoperationer som är aktiva under en transaktion. Först då tillämpas de faktiskt på transaktionsomfånget där du kan återställa dem.
Som en lite mer komplett lista, och bara med den mer klassiska Order/OrderItems
modellering som borde vara ganska bekant för de flesta med viss erfarenhet av relationstransaktioner:
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()
}
})()
Så jag skulle generellt rekommendera att anropa variabeln session
med gemener, eftersom detta är namnet på nyckeln för "options"-objektet där det krävs för alla operationer. Genom att hålla detta i gemener kan du också använda saker som ES6-objekttilldelningen:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Mongoosedokumentationen om transaktioner är också lite missvisande, eller åtminstone kan den vara mer beskrivande. Vad det refererar till som db
i exemplen är faktiskt Mongoose Connection-instansen, och inte den underliggande Db
eller till och med mongoose
global import eftersom vissa kan misstolka detta. Notera i listan och ovanstående utdrag att detta hämtas från mongoose.connect()
och bör hållas inom din kod som något du kan komma åt från en delad import.
Alternativt kan du till och med ta tag i detta i modulär kod via mongoose.connection
egendom, när som helst efter en anslutning har upprättats. Detta är vanligtvis säkert i saker som serverrutthanterare och liknande eftersom det kommer att finnas en databasanslutning när koden anropas.
Koden demonstrerar också session
användning i de olika modellmetoderna:
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Alla find()
baserade metoder och update()
eller insert()
och delete()
baserade metoder har alla ett sista "alternativblock" där denna sessionsnyckel och värde förväntas. save()
Metodens enda argument är detta alternativblock. Detta är vad som säger till MongoDB att tillämpa dessa åtgärder på den aktuella transaktionen på den refererade sessionen.
På ungefär samma sätt, innan en transaktion genomförs, alla förfrågningar om en find()
eller liknande som inte anger den session
alternativet ser inte tillståndet för data medan transaktionen pågår. Det modifierade datatillståndet är endast tillgängligt för andra operationer när transaktionen är klar. Observera att detta har effekter på skrivningar som beskrivs i dokumentationen.
När en "avbryt" utfärdas:
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Eventuella operationer på den aktiva transaktionen tas bort från staten och tillämpas inte. Som sådana är de inte synliga för efterföljande operationer. I exemplet här är värdet i dokumentet inkrementerat och kommer att visa ett hämtat värde på 5
på den aktuella sessionen. Men efter session.abortTransaction()
det tidigare tillståndet för dokumentet återställs. Observera att alla globala kontexter som inte läste data på samma session, inte ser den tillståndet ändras såvida det inte har bestämts.
Det borde ge den allmänna översikten. Det finns mer komplexitet som kan läggas till för att hantera olika nivåer av skrivfel och återförsök, men som redan täcks utförligt i dokumentationen och många exempel, eller kan besvaras på en mer specifik fråga.
Utdata
Som referens visas resultatet av den inkluderade listan här:
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
}
]
}
]