Você precisa incluir a
session
dentro das opções para todas as operações de leitura/gravação que estão ativas durante uma transação. Só então eles são realmente aplicados ao escopo da transação onde você pode revertê-los. Como uma lista um pouco mais completa, e apenas usando o mais clássico
Order/OrderItems
modelagem que deve ser bastante familiar para a maioria das pessoas com alguma experiência em transações relacionais: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()
}
})()
Então eu geralmente recomendo chamar a variável
session
em minúsculas, pois este é o nome da chave para o objeto "options" onde é necessário em todas as operações. Manter isso na convenção de letras minúsculas permite usar coisas como a atribuição do objeto ES6 também:const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Além disso, a documentação do mangusto sobre transações é um pouco enganosa, ou pelo menos poderia ser mais descritiva. A que se refere como
db
nos exemplos é na verdade a instância Mongoose Connection, e não o Db
subjacente ou até mesmo o mongoose
importação global, pois alguns podem interpretar mal isso. Observe na listagem e no trecho acima que isso é obtido de mongoose.connect()
e deve ser mantido em seu código como algo que você pode acessar de uma importação compartilhada. Alternativamente, você pode até pegar isso em código modular através do
mongoose.connection
propriedade, a qualquer momento depois uma conexão foi estabelecida. Isso geralmente é seguro dentro de coisas como manipuladores de rota do servidor e similares, pois haverá uma conexão com o banco de dados no momento em que o código for chamado. O código também demonstra a
session
uso nos diferentes métodos de modelo:let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
Todos os
find()
métodos baseados e o update()
ou insert()
e delete()
todos os métodos baseados têm um "bloco de opções" final onde esta chave e valor de sessão são esperados. O save()
o único argumento do método é este bloco de opções. Isso é o que diz ao MongoDB para aplicar essas ações à transação atual naquela sessão referenciada. Da mesma forma, antes de uma transação ser confirmada, qualquer solicitação para um
find()
ou similares que não especificam que session
opção não ver o estado dos dados enquanto essa transação estiver em andamento. O estado de dados modificado só está disponível para outras operações após a conclusão da transação. Observe que isso tem efeitos nas gravações, conforme abordado na documentação. Quando um "aborto" é emitido:
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Quaisquer operações na transação ativa são removidas do estado e não são aplicadas. Como tal, eles não são visíveis para as operações resultantes posteriormente. No exemplo aqui o valor no documento é incrementado e mostrará um valor recuperado de
5
na sessão atual. No entanto, após session.abortTransaction()
o estado anterior do documento é revertido. Observe que qualquer contexto global que não estava lendo dados na mesma sessão, não vê essa mudança de estado, a menos que seja confirmado. Isso deve dar a visão geral. Há mais complexidade que pode ser adicionada para lidar com vários níveis de falha de gravação e novas tentativas, mas isso já é amplamente abordado na documentação e em muitos exemplos, ou pode ser respondido a uma pergunta mais específica.
Saída
Para referência, a saída da listagem incluída é mostrada aqui:
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
}
]
}
]