Вам необходимо включить session
в опции для всех операций чтения / записи, которые активны во время транзакции.Только тогда они фактически применяются к области транзакции, где вы можете откатить их.
В качестве немного более полного списка, и просто с использованием более классического Order/OrderItems
моделирования, которое должно быть довольно знакомо большинству людей.с некоторым опытом работы с реляционными транзакциями:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trandemo';
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
в нижнем регистре, поскольку это имя ключа для объекта «options», который требуется для всех операций.,Сохранение этого в соглашении о нижнем регистре позволяет использовать такие вещи, как назначение объекта ES6:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Также документация mongoose по транзакциям немного вводит в заблуждение, или, по крайней мере, это может бытьболее описательный.В примерах это 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()
или аналогичные, которые не указывают это *Опция 1041 * не отображает состояние данных во время выполнения транзакции.Измененное состояние данных доступно для других операций только после завершения транзакции.Обратите внимание, что это влияет на запись, как описано в документации .
Когда выдается «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();
Все операции с активной транзакцией удаляются изсостояние и не применяются.Как таковые они не видны для последующих операций впоследствии.В приведенном здесь примере значение в документе увеличивается, и в текущем сеансе будет показано извлеченное значение 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
}
]
}
]