MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Come utilizzare la transazione MongoDB usando Mongoose?

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
      }
    ]
  }
]