MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Wie verwende ich die MongoDB-Transaktion mit Mongoose?

Sie müssen die session einschließen innerhalb der Optionen für alle Schreib-/Leseoperationen, die während einer Transaktion aktiv sind. Nur dann werden sie tatsächlich auf den Transaktionsbereich angewendet, wo Sie sie rückgängig machen können.

Als etwas vollständigere Auflistung und nur mit den klassischeren Order/OrderItems Modellierung, die den meisten Leuten mit etwas Erfahrung mit relationalen Transaktionen ziemlich vertraut sein sollte:

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()
  }

})()

Daher würde ich generell empfehlen, die Variable session aufzurufen in Kleinbuchstaben, da dies der Name des Schlüssels für das Objekt "Optionen" ist, wo er für alle Operationen benötigt wird. Wenn Sie die Kleinbuchstaben verwenden, können Sie auch Dinge wie die ES6-Objektzuweisung verwenden:

const conn = await mongoose.connect(uri, opts);

...

let session = await conn.startSession();
session.startTransaction();

Auch die Mongoose-Dokumentation zu Transaktionen ist ein wenig irreführend oder könnte zumindest aussagekräftiger sein. Was es als db bezeichnet in den Beispielen ist tatsächlich die Mongoose Connection-Instanz und nicht die zugrunde liegende Db oder sogar der mongoose globaler Import, da einige dies falsch interpretieren könnten. Beachten Sie in der Auflistung und dem obigen Auszug, dass dies von mongoose.connect() abgerufen wird und sollte in Ihrem Code als etwas aufbewahrt werden, auf das Sie über einen gemeinsamen Import zugreifen können.

Alternativ können Sie dies sogar in modularem Code über die mongoose.connection abrufen Eigentum, jederzeit nach eine Verbindung wurde hergestellt. Dies ist normalerweise innerhalb von Dingen wie Server-Route-Handlern und dergleichen sicher, da zu dem Zeitpunkt, zu dem dieser Code aufgerufen wird, eine Datenbankverbindung besteht.

Der Code demonstriert auch die session Verwendung in den verschiedenen Modellmethoden:

let [order, other] = await Order.insertMany([
  { name: 'Bill' },
  { name: 'Ted' }
], { session });

let fred = new Order({ name: 'Fred' });
await fred.save({ session });

Alle find() basierte Methoden und das update() oder insert() und delete() basierte Methoden haben alle einen abschließenden "Optionsblock", in dem dieser Sitzungsschlüssel und -wert erwartet werden. Das save() Das einzige Argument der Methode ist dieser Optionsblock. Dies weist MongoDB an, diese Aktionen auf die aktuelle Transaktion in dieser referenzierten Sitzung anzuwenden.

Auf die gleiche Weise, bevor eine Transaktion festgeschrieben wird, alle Anforderungen für ein find() oder ähnliches, die diese session nicht spezifizieren Option den Status der Daten nicht sehen, während diese Transaktion ausgeführt wird. Der geänderte Datenstatus ist nur für andere Operationen verfügbar, sobald die Transaktion abgeschlossen ist. Beachten Sie, dass dies Auswirkungen auf Schreibvorgänge hat, wie in der Dokumentation beschrieben.

Wenn ein "Abbruch" ausgegeben wird:

// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
  { order: order._id, itemName: 'Milk' },
  { $inc: { price: 1 } },
  { 'new': true, session }
);
log(result2);

await session.abortTransaction();

Alle Vorgänge für die aktive Transaktion werden aus dem Status entfernt und nicht angewendet. Als solche sind sie für nachfolgende Operationen nicht sichtbar. Im Beispiel hier wird der Wert im Dokument erhöht und zeigt einen abgerufenen Wert von 5 in der aktuellen Sitzung. Allerdings nach session.abortTransaction() der vorherige Zustand des Dokuments wird wiederhergestellt. Beachten Sie, dass jeder globale Kontext, der keine Daten in derselben Sitzung gelesen hat, diese Statusänderung nicht sieht, es sei denn, er wird festgeschrieben.

Das sollte den allgemeinen Überblick geben. Es gibt noch mehr Komplexität, die hinzugefügt werden kann, um unterschiedliche Grade von Schreibfehlern und Wiederholungen zu handhaben, aber das wird bereits ausführlich in der Dokumentation und vielen Beispielen behandelt oder kann auf eine spezifischere Frage beantwortet werden.

Ausgabe

Als Referenz wird hier die Ausgabe des enthaltenen Listings gezeigt:

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