Devi includere la session
all'interno delle opzioni per tutte le operazioni di lettura/scrittura attive durante una transazione. Solo allora vengono effettivamente applicati all'ambito della transazione in cui puoi ripristinarli.
Come elenco un po' più completo e semplicemente utilizzando il più classico Order/OrderItems
modellazione che dovrebbe essere abbastanza familiare alla maggior parte delle persone con esperienza di transazioni relazionali:
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()
}
})()
Quindi in genere consiglierei di chiamare la variabile session
in minuscolo, poiché questo è il nome della chiave per l'oggetto "opzioni" dove è richiesta su tutte le operazioni. Mantenerlo nella convenzione minuscola consente di utilizzare anche cose come l'assegnazione di oggetti ES6:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Anche la documentazione della mangusta sulle transazioni è un po' fuorviante, o almeno potrebbe essere più descrittiva. A cosa si riferisce come db
negli esempi è in realtà l'istanza di Mongoose Connection e non il Db
sottostante o anche la mongoose
importazione globale poiché alcuni potrebbero interpretarlo erroneamente. Nota nell'elenco e nell'estratto sopra questo è ottenuto da mongoose.connect()
e dovrebbe essere mantenuto all'interno del tuo codice come qualcosa a cui puoi accedere da un'importazione condivisa.
In alternativa puoi anche prenderlo in codice modulare tramite mongoose.connection
proprietà, in qualsiasi momento dopo è stata stabilita una connessione. Questo di solito è sicuro all'interno di cose come gestori di route del server e simili poiché ci sarà una connessione al database quando verrà chiamato il codice.
Il codice mostra anche la session
utilizzo nei diversi metodi del modello:
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Tutti i find()
metodi basati e update()
o insert()
e delete()
i metodi basati hanno tutti un "blocco opzioni" finale in cui sono previsti questa chiave e valore di sessione. Il save()
l'unico argomento del metodo è questo blocco di opzioni. Questo è ciò che dice a MongoDB di applicare queste azioni alla transazione corrente su quella sessione di riferimento.
Più o meno allo stesso modo, prima che una transazione venga confermata, qualsiasi richiesta per un find()
o simili che non specificano quella session
opzione non vedere lo stato dei dati mentre la transazione è in corso. Lo stato dei dati modificati è disponibile per altre operazioni solo una volta completata la transazione. Nota che questo ha effetti sulle scritture come descritto nella documentazione.
Quando viene emesso un "abort":
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Qualsiasi operazione sulla transazione attiva viene rimossa dallo stato e non viene applicata. In quanto tali non sono visibili alle operazioni risultanti in seguito. Nell'esempio qui il valore nel documento viene incrementato e mostrerà un valore recuperato di 5
sulla sessione corrente. Tuttavia dopo session.abortTransaction()
lo stato precedente del documento viene ripristinato. Nota che qualsiasi contesto globale che non stava leggendo i dati sulla stessa sessione, non vede quel cambiamento di stato a meno che non sia stato eseguito il commit.
Questo dovrebbe fornire una panoramica generale. È possibile aggiungere più complessità per gestire vari livelli di errori di scrittura e tentativi, ma è già ampiamente trattata nella documentazione e in molti esempi, oppure può trovare risposta a una domanda più specifica.
Uscita
Per riferimento, l'output dell'elenco incluso è mostrato qui:
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
}
]
}
]