javascript - 如何使用 Mongoose 使用 MongoDB 事务?
问题描述
我正在使用 MongoDB Atlas 云(https://cloud.mongodb.com/)和 Mongoose 库。
我尝试使用事务概念创建多个文档,但它不起作用。我没有收到任何错误。但是,回滚似乎无法正常工作。
应用程序.js
//*** more code here
var app = express();
require('./models/db');
//*** more code here
模型/db.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;
模型/user.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");
我的路由.js
const db = require("mongoose");
const User = require("./models/user");
router.post("/addusers", async (req, res, next) => {
const SESSION = await db.startSession();
await SESSION.startTransaction();
try {
const newUser = new User({
//*** data for user ***
});
await newUser.save();
//*** for test purpose, trigger some error ***
throw new Error("some error");
await SESSION.commitTransaction();
//*** return data
} catch (error) {
await SESSION.abortTransaction();
} finally {
SESSION.endSession();
}
});
上面的代码没有错误,但它仍然在数据库中创建用户。它假设回滚创建的用户并且集合应该是空的。
我不知道我在这里错过了什么。谁能告诉我这里有什么问题?
应用程序、模型、模式和路由器位于不同的文件中。
解决方案
您需要session
在事务期间处于活动状态的所有读/写操作的选项中包含 。只有这样它们才能真正应用于您能够回滚它们的事务范围。
作为一个更完整的清单,并且只使用更经典的Order/OrderItems
建模,对于大多数具有一些关系事务经验的人来说应该非常熟悉:
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()
}
})()
所以我通常建议session
以小写形式调用变量,因为这是所有操作都需要的“选项”对象的键名。将其保持在小写约定中也允许使用诸如 ES6 对象分配之类的东西:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
此外,关于交易的猫鼬文档有点误导,或者至少它可能更具描述性。它在示例中所指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()
orinsert()
和delete()
的方法都有一个最终的“选项块”,其中需要这个会话键和值。该save()
方法的唯一参数是这个选项块。这就是告诉 MongoDB 将这些操作应用于该引用会话上的当前事务的原因。
以同样的方式,在提交事务之前,任何find()
未指定该session
选项的请求或类似请求在该事务进行时都不会看到数据的状态。修改后的数据状态仅在事务完成后可用于其他操作。请注意,这会对文档中的写入产生影响。
当发出“中止”时:
// 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
}
]
}
]
推荐阅读
- jquery - 当用户保存详细信息时,Jquery 将焦点放在第一个输入框
- r - 从 R 中的 BCI 数据中提取一列
- python - 如何在 Python 中使用枕头裁剪宽高比为 1:1 的图像?
- python - 如何检查 django 中的 sqlite 字段是否可用
- git - 在系统中添加 2 个 git repos
- spring - 空远程目录时 Spring Integration Flow API 不响应
- matrix - 如何证明以下矩阵的秩>= n-1?
- node.js - 任何可用于将任务添加到 Microsoft Teams 中的 Planner 应用程序的 API
- python - Firefox 中的数据顺序不正确
- javascript - 迭代对象数组并更新属性