Skip to content

fix(destroy): Properties updated in a beforeDestroy hook are now pers… - #9319

Merged
sushantdhiman merged 2 commits into
sequelize:masterfrom
ddolcimascolo:issue-9318
Jul 22, 2018
Merged

sushantdhiman merged 2 commits into
sequelize:masterfrom
ddolcimascolo:issue-9318

Conversation

@ddolcimascolo

@ddolcimascolo ddolcimascolo commented Apr 17, 2018 •

Copy link
Copy Markdown
Contributor

…isted on soft delete. Closes #9318

Pull Request check-list

Please make sure to review and check all of these items:

  • Does npm run test or npm run test-DIALECT pass with this change (including linting)?
  • Does the description below contain a link to an existing issue (Closes #[issue]) or a description of the issue you are solving?
  • Have you added new tests to prevent regressions?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
  • Did you follow the commit message conventions explained in CONTRIBUTING.md?

Description of change

When getting the list of things to update in the database to perform a soft delete, the code is now considering changed values (through Model.changed()) instead of only updating deletedAt.

Closes issue #9318

@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

I think this should be applied on 4.x branch also, but I'm unsure if I should submit a new PR targetting another branch, or if you automagically cherry-pick commits to backport...

Just tell me, I'm happy to open another PR if needs be.

@codecov

codecov Bot commented Apr 17, 2018 •

Copy link
Copy Markdown

Codecov Report

Merging #9319 into master will not change coverage.
The diff coverage is 100%.

@sushantdhiman sushantdhiman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few changes

Comment thread test/integration/model.test.js Outdated
});
});

it('sets other changed values when soft deleting and a beforeDestroy hooks kicks in', function() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to https://github.com/sequelize/sequelize/blob/master/test/integration/hooks/destroy.test.js

Please create a new group for paranoid, under that place this test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread lib/model.js Outdated
const attribute = this.constructor.rawAttributes[this.constructor._timestampAttributes.deletedAt];
const field = attribute.field || this.constructor._timestampAttributes.deletedAt;
const values = {};
const values = Utils.mapValueFieldNames(this, this.changed() || [], this.constructor);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be mapValueFieldNames(this.dataValues,....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread test/integration/model.test.js Outdated
});

it('sets other changed values when soft deleting and a beforeDestroy hooks kicks in', function() {
const ParanoidUser = this.sequelize.define('ParanoidUser', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a test where you try to update a virtual field in hook and see it doesn't get updated (i.e. it should not throw error for unknown column)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@sushantdhiman sushantdhiman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, some cosmetic changes

Comment thread test/integration/hooks/destroy.test.js Outdated
});

describe('with paranoid mode enabled', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra line

Comment thread test/integration/hooks/destroy.test.js Outdated
expect(user.virtualField).to.equal(0);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra line

Comment thread test/integration/hooks/destroy.test.js Outdated
});

});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra line

@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

@sushantdhiman Done, sorry for the extra blank lines

@sushantdhiman
sushantdhiman requested a review from a team April 19, 2018 08:52
@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

Thanks for the appoval, what about the backport to v4 ?

@sushantdhiman

Copy link
Copy Markdown
Contributor

Thanks for the appoval, what about the backport to v4 ?

I'll cherry pick commit from master and port to v4 :)

@sushantdhiman

Copy link
Copy Markdown
Contributor

I was checking other use of this hook, we need to apply this change to

sequelize/lib/model.js

Lines 2546 to 2568 in 4caa17c

// Get daos and run beforeDestroy hook on each record individually
if (options.individualHooks) {
return this.findAll({where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark})
.map(instance => this.runHooks('beforeDestroy', instance, options).then(() => instance))
.then(_instances => {
instances = _instances;
});
}
}).then(() => {
// Run delete query (or update if paranoid)
if (this._timestampAttributes.deletedAt && !options.force) {
// Set query type appropriately when running soft delete
options.type = QueryTypes.BULKUPDATE;
const attrValueHash = {};
const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt];
const field = this.rawAttributes[this._timestampAttributes.deletedAt].field;
const where = {};
where[field] = deletedAtAttribute.hasOwnProperty('defaultValue') ? deletedAtAttribute.defaultValue : null;
attrValueHash[field] = Utils.now(this.sequelize.options.dialect);
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes);

So individualHooks: true cases can use this as well. Will need extra tests.

Also Model.destroy is using is Utils.now but Model.prototype.destroy is setting new Date, probably both need to use Utils.now

This change may be unexpected for some users and definitely change hook behaviour (#9318 (comment)) so its better to keep this in v5

@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

Fine to keep this v5 only.
Regarding your latest comments I can handle the changes and add tests for the bulk destroy use cases. Will try to do this later this day and update the PR.

Cheers

@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

@sushantdhiman Sorry, I'm puzzled 😀

Model.destroy will indeed call the beforeDestroy hook on every instance impacted by the bulk destroy, but then how is the code supposed to "know" that some values changed on the instances, how is the code supposed to translate that in the bulk UPDATE statement that is issued to the database ?
We could even imagine that the hook has changed different attributes on different instances...

I'm thinking that the code should individually call destroy on each instance instead of doing a bulk operation, if and only if individualHooks: true is passed.

That would sum up to something like (pseudo-code):

runHook('beforeBulkDestroy')
if (individualHooks) {
  for each instance returned by findAll()
    instance.destroy()
} else {
  doTheBulkOperation
}
runHook('afterBulkDestroy')

Calling instance.destroy() will indeed run the before/afterDestroy hooks on it, which is what is expected by individualHooks: true anyway.

WDYT?

@sushantdhiman

Copy link
Copy Markdown
Contributor

@ddolcimascolo Good question and your pseudo-code is exactly what we should do (like the update case), I reckon we need this change for restore process as well.

@ddolcimascolo

Copy link
Copy Markdown
Contributor Author

Hey guys (and @sushantdhiman),

Can you share the status of this contribution? Do you need more code from me or can we consider it good to merge, eventually opening another issue to track the refactoring of the bulk 'destroy' operation?

For what it's worth, we're using this patch in production for several weeks now without any issue. It fits well with bonaval/sequelize-temporal#7 which I contributed too.

Thanks,
Regards,

David

@sushantdhiman
sushantdhiman merged commit 0fe77d6 into sequelize:master Jul 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants