Musisz dołączyć session
w ramach opcji dla wszystkich operacji odczytu/zapisu, które są aktywne podczas transakcji. Dopiero wtedy są faktycznie stosowane do zakresu transakcji, w którym można je wycofać.
Jako nieco bardziej kompletna lista i tylko przy użyciu bardziej klasycznych Order/OrderItems
modelowanie, które powinno być dobrze znane większości osób z pewnym doświadczeniem w transakcjach relacyjnych:
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()
}
})()
Więc ogólnie polecam wywołanie zmiennej session
małymi literami, ponieważ jest to nazwa klucza dla obiektu „opcje”, w którym jest on wymagany we wszystkich operacjach. Utrzymanie tego w konwencji małych liter pozwala również na użycie takich rzeczy, jak przypisanie obiektu ES6:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Również dokumentacja dotycząca mangusty dotycząca transakcji jest trochę myląca, a przynajmniej może być bardziej opisowa. Do czego odnosi się jako db
w przykładach jest w rzeczywistości instancją Mongoose Connection, a nie bazowym Db
a nawet mongoose
globalny import, ponieważ niektórzy mogą to błędnie zinterpretować. Uwaga na liście i powyższym fragmencie jest to uzyskiwane z mongoose.connect()
i powinien być przechowywany w kodzie jako coś, do czego można uzyskać dostęp z udostępnionego importu.
Alternatywnie możesz nawet pobrać to w kodzie modułowym za pomocą mongoose.connection
właściwość, w dowolnym momencie po połączenie zostało nawiązane. Jest to zwykle bezpieczne wewnątrz elementów, takich jak programy obsługi tras serwera i tym podobne, ponieważ do czasu wywołania kodu będzie połączenie z bazą danych.
Kod demonstruje również session
zastosowanie w różnych metodach modelowych:
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Wszystkie find()
oparte na metodach i update()
lub insert()
i delete()
Wszystkie metody oparte mają końcowy „blok opcji”, w którym oczekiwany jest ten klucz sesji i wartość. save()
jedynym argumentem metody jest ten blok opcji. To właśnie mówi MongoDB, aby zastosować te działania do bieżącej transakcji w tej sesji, do której się odwołuje.
W podobny sposób, zanim transakcja zostanie zatwierdzona, wszelkie żądania find()
lub podobne, które nie określają, że session
opcja nie wyświetlaj stanu danych, gdy transakcja jest w toku. Zmodyfikowany stan danych jest dostępny tylko dla innych operacji po zakończeniu transakcji. Zauważ, że ma to wpływ na zapisy opisane w dokumentacji.
Po wydaniu „przerwania”:
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Wszelkie operacje na aktywnej transakcji są usuwane ze stanu i nie są stosowane. Jako takie nie są widoczne dla późniejszych operacji. W tym przykładzie wartość w dokumencie jest zwiększana i pokaże pobraną wartość 5
w bieżącej sesji. Jednak po session.abortTransaction()
poprzedni stan dokumentu zostaje przywrócony. Zwróć uwagę, że żaden kontekst globalny, który nie odczytywał danych w tej samej sesji, nie widzi zmiany stanu, dopóki nie zostanie zatwierdzony.
To powinno dać ogólny przegląd. Istnieje więcej złożoności, które można dodać, aby poradzić sobie z różnymi poziomami błędów zapisu i ponownych prób, ale jest to już obszernie omówione w dokumentacji i wielu przykładach lub można uzyskać odpowiedź na bardziej szczegółowe pytanie.
Wyjście
Dla porównania, dane wyjściowe dołączonej listy są pokazane tutaj:
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
}
]
}
]