Vous devez inclure la session
dans les options pour toutes les opérations de lecture/écriture qui sont actives pendant une transaction. Ce n'est qu'alors qu'ils sont réellement appliqués à la portée de la transaction où vous pouvez les annuler.
Comme une liste un peu plus complète, et en utilisant simplement le plus classique Order/OrderItems
modélisation qui devrait être assez familière à la plupart des personnes ayant une certaine expérience des transactions relationnelles :
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()
}
})()
Je recommanderais donc généralement d'appeler la variable session
en minuscules, puisqu'il s'agit du nom de la clé de l'objet "options" où il est requis sur toutes les opérations. Garder cela dans la convention en minuscules permet également d'utiliser des éléments tels que l'affectation d'objet ES6 :
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
De plus, la documentation de la mangouste sur les transactions est un peu trompeuse, ou du moins elle pourrait être plus descriptive. Ce qu'il appelle db
dans les exemples est en fait l'instance Mongoose Connection, et non le sous-jacent Db
ou encore la mongoose
importation mondiale car certains peuvent mal interpréter cela. Notez dans la liste et l'extrait ci-dessus que cela est obtenu à partir de mongoose.connect()
et doit être conservé dans votre code comme quelque chose auquel vous pouvez accéder à partir d'une importation partagée.
Alternativement, vous pouvez même saisir ceci dans le code modulaire via le mongoose.connection
propriété, à tout moment après une connexion a été établie. Ceci est généralement sécurisé à l'intérieur d'éléments tels que les gestionnaires de routage de serveur, etc., car il y aura une connexion à la base de données au moment où le code sera appelé.
Le code illustre également la session
utilisation dans les différentes méthodes du modèle :
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Tous les find()
méthodes basées sur update()
ou insert()
et delete()
les méthodes basées ont toutes un "bloc d'options" final où cette clé de session et cette valeur sont attendues. Le save()
le seul argument de la méthode est ce bloc d'options. C'est ce qui indique à MongoDB d'appliquer ces actions à la transaction en cours sur cette session référencée.
De la même manière, avant qu'une transaction ne soit validée, toute demande de find()
ou similaire qui ne spécifient pas cette session
option ne voit pas l'état des données pendant que cette transaction est en cours. L'état des données modifiées n'est disponible pour les autres opérations qu'une fois la transaction terminée. Notez que cela a des effets sur les écritures comme indiqué dans la documentation.
Lorsqu'un "abandon" est émis :
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Toutes les opérations sur la transaction active sont supprimées de l'état et ne sont pas appliquées. En tant que tels, ils ne sont pas visibles pour les opérations résultantes par la suite. Dans l'exemple ici, la valeur dans le document est incrémentée et affichera une valeur récupérée de 5
sur la session en cours. Cependant après session.abortTransaction()
l'état précédent du document est rétabli. Notez que tout contexte global qui ne lisait pas de données sur la même session ne voit pas ce changement d'état à moins qu'il ne soit validé.
Cela devrait donner un aperçu général. Il y a plus de complexité qui peut être ajoutée pour gérer différents niveaux d'échec d'écriture et de tentatives, mais cela est déjà largement couvert dans la documentation et de nombreux exemples, ou peut être répondu à une question plus spécifique.
Sortie
Pour référence, la sortie de la liste incluse est affichée ici :
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
}
]
}
]