猫鼬 |中间件 |抛出错误时由 pre/post 钩子执行的回滚操作 [英] Mongoose | Middleware | Rollback operations performed by pre/post hooks when an error is thrown

查看:30
本文介绍了猫鼬 |中间件 |抛出错误时由 pre/post 钩子执行的回滚操作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑下面的代码,如何实现事务以确保 someParentDocument 不会被删除,并且在钩子内执行的任何操作都被回滚,当任何一个错误被抛出时钩子?

Considering the code below, how can a transaction be implemented in order to ensure that someParentDocument doesn't get deleted and any operations performed inside the hooks are rolledback, when an error is thrown in any of the hooks?


const parentSchema = new mongoose.Schema({
    name: String,
    children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});

const childSchema = new mongoose.Schema({
    name: String,
    parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});

parentSchema.pre("remove", async function(next){
    // Add and remove documents to Parent and Child...
    //...
    next();
});

parentSchema.post("remove", async function(parent){
    throw new Error("Exception!");
});


// (...)
try {
    await someParentDocument.delete();  // <-- will trigger the hooks above
} catch {}

推荐答案

TLDR;Mongoose 中间件不是为此而设计的.

这种插​​入事务的方法实际上是在修补中间件功能,您实际上是在创建一个与 mongoose 中间件完全分离的 api.

TLDR; Mongoose middleware was not designed for this.

This method of inserting transactions is actually patching the middleware functionality, and you are essentially creating an api completely separate from the mongoose middleware.

最好在单独的函数中反转删除查询的逻辑.

What would be better is inverting the logic for your remove query in a separate function.

允许事务处理方法发挥其魔力,并为您的父模型创建一个单独的 remove 方法.Mongoose 将 mongodb.ClientSession.prototype.withTransactionmongoose.Connection.prototype.transaction 包装在一起,我们甚至不必实例化或管理会话!看看这个和下面那个的长度有什么不同.您可以省去记住中间件内部结构的麻烦,而代价是使用一个单独的功能.

Allow a transaction handling method to do its magic, and create a separate remove method for your parent model. Mongoose wraps mongodb.ClientSession.prototype.withTransaction with mongoose.Connection.prototype.transaction and we don't even have to instantiate or manage a session! Look at the different between the length of this and that below. And you save the mental headache of remembering the internals of that middleware at the cost of one separate function.


const parentSchema = new mongoose.Schema({
    name: String,
    children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});

const childSchema = new mongoose.Schema({
    name: String,
    parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});

// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
    // The document's connection
    const db = parent.db;

    // This handles everything with the transaction for us, including retries
    // session, commits, aborts, etc.
    await db.transaction(async function (session) {
        // Make sure to associate all actions with the session
        await parent.remove({ session });
        await db
            .model("Child")
            .deleteMany({ _id: { $in: parent.children } })
            .session(session);
    });

    // And done!
}

小扩展

另一种简化此操作的方法是注册一个仅继承会话的中间件如果_查询已注册一个会话.如果事务尚未启动,可能会抛出错误.

Small Extension

Another way to make this easy, is to register a middleware which simply inherits a session iff_ the query has one registered. Maybe throw an error if a transaction has not been started.

const parentSchema = new mongoose.Schema({
    name: String,
    children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});

const childSchema = new mongoose.Schema({
    name: String,
    parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});

parentSchema.pre("remove", async function () {
    // Look how easy!! Just make sure to pass a transactional 
    // session to the removal
    await this.db
        .model("Child")
        .deleteMany({ _id: { $in: parent.children } })
        .session(this.$session());

    // // If you want to: throw an error/warning if you forgot to add a session
    // // and transaction
    // if(!this.$session() || !this.$session().inTransaction()) {
    //    throw new Error("HEY YOU FORGOT A TRANSACTION.");
    // }
});

// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
    db.transaction(async function(session) {
        await parent.remove({ session });
    });
}

风险和复杂的解决方案

这行得通,而且非常复杂.不建议.有一天可能会中断,因为它依赖于 mongoose API 的复杂性.我不知道我为什么要编码这个,请不要将它包含在你的项目中.

import mongoose from "mongoose";
import mongodb from "mongodb";

const parentSchema = new mongoose.Schema({
    name: String,
    children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});

const childSchema = new mongoose.Schema({
    name: String,
    parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});

// Choose a transaction timeout
const TRANSACTION_TIMEOUT = 120000; // milliseconds

// No need for next() callback if using an async function.
parentSchema.pre("remove", async function () {
    // `this` refers to the document, not the query
    let session = this.$session();

    // Check if this op is already part of a session, and start one if not.
    if (!session) {
        // `this.db` refers to the documents's connection.
        session = await this.db.startSession();

        // Set the document's associated session.
        this.$session(session);

        // Note if you created the session, so post can clean it up.
        this.$locals.localSession = true;

        //
    }

    // Check if already in transaction.
    if (!session.inTransaction()) {
        await session.startTransaction();

        // Note if you created transaction.
        this.$locals.localTransaction = true;

        // If you want a timeout
        this.$locals.startTime = new Date();
    }

    // Let's assume that we need to remove all parent references in the
    // children. (just add session-associated ops to extend this)
    await this.db
        .model("Child") // Child model of this connection
        .updateMany(
            { _id: { $in: this.children } },
            { $unset: { parent: true } }
        )
        .session(session);
});

parentSchema.post("remove", async function (parent) {
    if (this.$locals.localTransaction) {
        // Here, there may be an error when we commit, so we need to check if it
        // is a 'retryable' error, then retry if so.
        try {
            await this.$session().commitTransaction();
        } catch (err) {
            if (
                err instanceof mongodb.MongoError &&
                err.hasErrorLabel("TransientTransactionError") &&
                new Date() - this.$locals.startTime < TRANSACTION_TIMEOUT
            ) {
                await parent.remove({ session: this.$session() });
            } else {
                throw err;
            }
        }
    }

    if (this.$locals.localSession) {
        await this.$session().endSession();
        this.$session(null);
    }
});

// Specific error handling middleware if its really time to abort (clean up
// the injections)
parentSchema.post("remove", async function (err, doc, next) {
    if (this.$locals.localTransaction) {
        await this.$session().abortTransaction();
    }

    if (this.$locals.localSession) {
        await this.$session().endSession();
        this.$session(null);
    }

    next(err);
});

这篇关于猫鼬 |中间件 |抛出错误时由 pre/post 钩子执行的回滚操作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆