TLDR; Mongoose middleware er ikke designet til dette.
Denne metode til at indsætte transaktioner er faktisk at lappe middleware-funktionaliteten, og du opretter i det væsentlige et api fuldstændig adskilt fra mongoose
middleware.
Hvad ville være bedre er at invertere logikken for din fjernelsesforespørgsel i en separat funktion.
Simpel og tilsigtet løsning
Tillad en transaktionshåndteringsmetode at gøre sin magi, og opret en separat fjernelsesmetode for din overordnede model. Mongoose ombryder mongodb.ClientSession.prototype.withTransaction
med mongoose.Connection.prototype.transaction
og vi behøver ikke engang at instansiere eller administrere en session! Se på forskellen mellem længden af dette og det nedenfor. Og du sparer den mentale hovedpine ved at huske det indre af denne middleware på bekostning af én separat funktion.
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!
}
Lille udvidelse
En anden måde at gøre dette let på er at registrere en middleware, som simpelthen arver en session iff _ forespørgslen har én registreret. Måske smide en fejl, hvis en transaktion ikke er startet.
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 });
});
}
Risikofuld og kompleks løsning
Dette virker, og er totalt, frygteligt komplekst. Ikke anbefalet. Vil sandsynligvis gå i stykker en dag, fordi den er afhængig af forviklingerne af mongoose API. Jeg ved ikke, hvorfor jeg kodede dette, medtag det ikke i dine projekter .
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);
});