我正在使用MongoDB Atlas cloud(https://cloud.mongodb.com/)和Mongoose库。
我尝试使用事务处理概念创建多个文档,但是它不起作用。我没有任何错误。但是,回滚似乎无法正常工作。
app.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");
myroute.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 Object分配之类的东西:
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()
或insert()
和delete()
基础的方法都有一个最终的“选项框”里这个会话密钥和价值的预期。该save()
方法的唯一参数是此选项块。这就是告诉MongoDB将这些操作应用于该引用会话上的当前事务的原因。
以几乎相同的方式,在提交事务之前find()
,未指定该session
选项的任何对a 或类似请求均不会在该事务进行中看到数据状态。事务完成后,修改后的数据状态仅对其他操作可用。请注意,这会影响文档中介绍的写入。
发出“中止”时:
// 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 } ] } ]