-
-
Save jordanell/d2f6bd69a40fbe9e4976a5baf8cc1d2a to your computer and use it in GitHub Desktop.
import paranoidDeleteCascade from './helpers/paranoidDeleteCascade'; | |
// Patch the paranoid delete functionality of Sequelize | |
sequelize.addHook('afterDestroy', paranoidDeleteCascade(db)); |
import isArray from 'lodash/isArray'; | |
import map from 'lodash/map'; | |
const paranoidDeleteCascade = (models) => | |
async (instance, options, next) => { | |
// Only operate on paranoid models | |
if (!instance.$modelOptions.paranoid) { | |
return next(); | |
} | |
const modelName = instance.$modelOptions.name.singular; | |
await Promise.all( | |
// Go over all associations of the instance model, and delete if needed | |
map(models[modelName].associations, async (association) => { | |
try { | |
// Only delete if cascade is set up correctly | |
if (association.options.onDelete !== 'CASCADE') { | |
return true; | |
} | |
let relationModel = association.target; | |
const getOptions = { transaction: options.transaction }; | |
// Handle "through" cases | |
if (association.through) { | |
relationModel = association.through.model; | |
// Include the id of the through model instance | |
getOptions.include = [{ | |
model: relationModel, | |
}]; | |
} | |
// Load id(s) of association | |
const instances = await instance[`get${association.as}`](getOptions); | |
if (isArray(instances)) { | |
// Association has no results so nothing to delete | |
if (instances.length === 0) { | |
return true; | |
} | |
// Delete all individually as bulk delete doesn't cascase in sequelize | |
return await Promise.all(instances.map(i => i.destroy(Object.assign({}, options, { individualHooks: true })))); | |
} | |
// Association is not set, so nothing to delete | |
if (!instances) { | |
return true; | |
} | |
return await instances.destroy(options); | |
} catch (error) { | |
// If we had issues deleting, we have bigger problems | |
Promise.resolve(true); | |
} | |
return true; | |
}) | |
); | |
return next(); | |
}; | |
export default paranoidDeleteCascade; |
With this hook, transactions won't rollback the effect of model.destroy
.
To fix this, we should pass down the transaction to the underlying instance.destroy
like so:
return await Promise.all(instances.map(i => i.destroy({ transaction: options.transaction })));
// [...]
return await instances.destroy({ transaction: options.transaction });
Transactions also need to be passed to the helper that load associations.
const getOptions = { transaction: options.transaction };
Sorry, how can i use that. I tried to mount it like this:
import paranoidDeleteCascade from "./paranoidDeleteCascade";
const Sequelize = require("sequelize");
require("dotenv").config();
export const sequelizeConnection = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
port: process.env.DB_PORT,
host: process.env.DB_HOST, //change this with docker host
dialect: process.env.DB_DIALECT
}
);
sequelizeConnection.addHook("afterDestroy", paranoidDeleteCascade(sequelizeConnection));
is that okei ? It seems not to work
Here an example of association
ProductPrice.belongsTo(Product, {
foreignKey: "productId",
onDelete: "CASCADE",
hooks: "true"
});
Product.hasMany(ProductPrice, {
as: "productPrices",
onDelete: "CASCADE",
hooks: true,
constraints: true
});
@Christian-Nja, on this line, you need to pass an object containing yours models to paranoidDeleteCascade
.
sequelizeConnection.addHook("afterDestroy", paranoidDeleteCascade(models));
@AntoineGrandchamp thank you! I inserted my model like this:
sequelizeConnection.addHook("afterDestroy", paranoidDeleteCascade({
product: Product,
productDescription: ProductDescription,
})
);
the relations are:
ProductDescription.belongsTo(Product, {
foreignKey: "productId"
});
Product.hasMany(ProductDescription, {
as: "productDescriptions",
onDelete: "CASCADE",
hooks: true
});
And the models:
const Sequelize = require("sequelize");
import { sequelizeConnection } from "../mySqlDBConnection";
export const Product = sequelizeConnection.define(
"product",
{
id: {
defaultValue: Sequelize.UUIDV4,
type: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
shopCode: {
type: Sequelize.STRING,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
},
barCode: {
type: Sequelize.STRING
},
quantity: {
type: Sequelize.INTEGER,
allowNull: false
},
state: {
type: Sequelize.STRING,
allowNull: false
},
deleteReason: {
type: Sequelize.STRING
}
},
{
tableName: "product",
freezeTableName: true,
paranoid: true,
timestamps: true
},
{
indexes: [
{
unique: true,
fields: ["id", "barCode", "shopCode"]
}
]
}
);
const Sequelize = require("sequelize");
import { sequelizeConnection } from "../mySqlDBConnection";
export const ProductDescription = sequelizeConnection.define(
"product_description",
{
id: {
defaultValue: Sequelize.UUIDV4,
type: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
description: {
type: Sequelize.STRING
},
importance: {
type: Sequelize.INTEGER
},
productId: {
type: Sequelize.INTEGER
},
deleteReason: {
type: Sequelize.STRING
}
},
{
tableName: "product_description",
freezeTableName: true,
paranoid: true,
timestamps: true
},
{
indexes: [
{
unique: true,
fields: ["id"]
}
]
}
);
But it still doesn't work ...
I confirm that this does work, great idea man, it might put some load on your server though specially if you're soft deleting large amount of data with a lot of associations.
One hint though, you need to the "individualHooks: true," property to the destroy options for the "afterDestroy" hook to run, otherwise it'll the "afterDestroy" won't fire but the "afterBulkDestroy" will fire instead.
I'm using sequelize version 5.22.3.
await db[model].destroy({ where: { id: item.id }, transaction: transaction, individualHooks: true, });
Thanks for the feedback @gostavee and @AntoineGrandchamp . I've incorporated your suggestions.
nice. :)