如何使用Mongoose使用MongoDB事务?

时间:2018-11-22 17:08:35

标签: javascript node.js mongodb mongoose mongodb-query

我正在使用MongoDB Atlas云(https://cloud.mongodb.com/)和Mongoose库。

我尝试使用事务处理概念创建多个文档,但是它不起作用。 我没有任何错误。但是,回滚似乎无法正常工作。

app.js

 'mat-icon' is not a known element:
1. If 'mat-icon' is an Angular component, then verify that it is part of this module.
2. If 'mat-icon' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("eld>
    <input matInput placeholder="Enter your password" [type]="hide ? 'password' : 'text'">
    [ERROR ->]<mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility_off' : 'visibility'}}</mat-icon>
  </"): ng:///AppModule/ApplicationComponent.html@4:4
'mat-form-field' is not a known element:
1. If 'mat-form-field' is an Angular component, then verify that it is part of this module.
2. If 'mat-form-field' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<h1>Products List</h1>
<div class="example-container">
  [ERROR ->]<mat-form-field>
    <input matInput placeholder="Enter your password" [type]="hide ? 'password' : 't"): ng:///AppModule/ApplicationComponent.html@2:2
    at syntaxError (compiler.js:2547)
    at TemplateParser.push../node_modules/@angular/compiler/fesm5/compiler.js.TemplateParser.parse (compiler.js:19495)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._parseTemplate (compiler.js:25041)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileTemplate (compiler.js:25028)
    at compiler.js:24971
    at Set.forEach (<anonymous>)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileComponents (compiler.js:24971)
    at compiler.js:24881
    at Object.then (compiler.js:2538)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileModuleAndComponents (compiler.js:24880)

models / db.js

//*** more code here

var app = express();

require('./models/db');

//*** more code here

models / user.js

var mongoose = require( 'mongoose' );

// Build the connection string
var dbURI = 'mongodb+srv://mydb:pass@cluster0-****.mongodb.net/mydb?retryWrites=true';

// Create the database connection
mongoose.connect(dbURI, {
  useCreateIndex: true,
  useNewUrlParser: true,
});

// Get Mongoose to use the global promise library
mongoose.Promise = global.Promise;

myroute.js

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  userName: {
    type: String,
    required: true
  },
  pass: {
    type: String,
    select: false
  }
});

module.exports = mongoose.model("User", UserSchema, "user");

以上代码可正常运行,但仍会在数据库中创建用户。它假定要回滚创建的用户,并且集合应该为空。

我不知道我在这里错过了什么。有人可以让我知道这是怎么回事吗?

应用,模型,架构和路由器位于不同的文件中。

1 个答案:

答案 0 :(得分:3)

您需要在事务期间处于活动状态的所有读/写操作的选项中包括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,因为这是“选项”对象的键名,在所有操作中都需要它。将其保持为小写约定还允许使用ES6对象分配之类的东西:

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

...

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

猫鼬documentation on transactions还是有点误导,或者至少可以更具描述性。在示例中,它所指的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()选项的session或类似请求都不会在该事务进行中看到数据状态。事务完成后,修改后的数据状态仅对其他操作可用。请注意,这会对documentation中所述的写入产生影响。

发出“中止”时:

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