TLDR; Il middleware Mongoose non è stato progettato per questo.
Questo metodo di inserimento delle transazioni sta effettivamente riparando la funzionalità del middleware e stai essenzialmente creando un'API completamente separata da mongoose
middleware.
Sarebbe meglio invertire la logica per la tua query di rimozione in una funzione separata.
Soluzione semplice e prevista
Consenti a un metodo di gestione delle transazioni di fare la sua magia e crea un metodo di rimozione separato per il tuo modello padre. Mongoose avvolge mongodb.ClientSession.prototype.withTransaction
con mongoose.Connection.prototype.transaction
e non dobbiamo nemmeno creare un'istanza o gestire una sessione! Guarda la differenza tra la lunghezza di questo e quello di seguito. E risparmi il mal di testa mentale di ricordare gli interni di quel middleware al costo di una funzione separata.
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!
}
Piccola estensione
Un altro modo per semplificare questa operazione è registrare un middleware che eredita semplicemente una sessione iff _ la query ne ha una registrata. Forse genera un errore se una transazione non è stata avviata.
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 });
});
}
Soluzione rischiosa e complessa
Funziona, ed è totalmente, orribilmente complesso. Non consigliato. Probabilmente si romperà un giorno perché si basa sulla complessità dell'API della mangusta. Non so perché l'ho codificato, per favore non includerlo nei tuoi progetti .
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);
});