MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Mangouste | Intergiciel | Opérations de restauration effectuées par les hooks pré/post lorsqu'une erreur est générée

TLDR ; Le middleware Mongoose n'a pas été conçu pour cela.

Cette méthode d'insertion de transactions corrige en fait la fonctionnalité du middleware, et vous créez essentiellement une API complètement séparée de la mongoose middleware.

Ce qui serait mieux, c'est d'inverser la logique de votre requête de suppression dans une fonction distincte.

Solution simple et intentionnelle

Laissez une méthode de gestion des transactions faire sa magie et créez une méthode de suppression distincte pour votre modèle parent. Mongoose enveloppe mongodb.ClientSession.prototype.withTransaction avec mongoose.Connection.prototype.transaction et nous n'avons même pas besoin d'instancier ou de gérer une session ! Regardez la différence entre la longueur de ceci et cela ci-dessous. Et vous évitez le casse-tête mental lié à la mémorisation des éléments internes de ce middleware au prix d'une fonction distincte.


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

Petite extension

Une autre façon de rendre cela facile est d'enregistrer un middleware qui hérite simplement d'une session iff _ la requête en a un enregistré. Peut-être lancer une erreur si une transaction n'a pas été démarrée.

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

Solution risquée et complexe

Cela fonctionne, et est totalement, horriblement complexe. Non recommandé. Va probablement casser un jour car il repose sur les subtilités de l'API mangouste. Je ne sais pas pourquoi j'ai codé ceci, veuillez ne pas l'inclure dans vos projets .

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