Problem

hook_entity/insert/update/delete() are currently used for storage operations and reactions. This is in particular problematic, when reactions involve storing the same objects again and lead to issues in the past, e.g.:
#1433288: Integrity constraint violation when saving a user account after creation
#1146244: node_access integrity constraint violation on module_invoke_all('node_' . $op, $node);

But moreover, it does not work well with our usage of transactions. Right now the transaction includes all those hooks, but any module reacting about an entity being saved should not be included in the transaction of the entity being saved.

Proposed resolution

Use hook_entity_load/insert/update/delete() for reactions only
Use fields and their methods (insert(), update(), getValue) for storage data that is considered being part of the entity, for an example of that see #1980822: Support any entity with path.module.

Remaining tasks

Update storage controllers to end transactions before invoking hooks
Update documentation of hooks and field methods to reflect this

User interface changes

-

API changes

Not really, but the intended / best practice hook usage is clarified.
Entity insert/update hook move out of database transactions

#221081: Entity cache out of sync and inconsistent when saving/deleting entities
#597236: Add entity caching to core
#2134079: Storage-related methods are not invoked for computed fields

Original report by @Dean Reilly

When the node entity was changed into a classed object (#1361234: Make the node entity a classed object) the order of the insert and edit hooks and the writing of node access records was rearranged.

Drupal 8
http://drupalcode.org/project/drupal.git/blob/HEAD:/core/modules/node/li...

Drupal 7
http://drupalcode.org/project/drupal.git/blob/refs/heads/7.x:/modules/no...

Personally, I think this is a good thing as it avoids the problem described in this issue #1146244: node_access integrity constraint violation on module_invoke_all('node_' . $op, $node);.

However, it does mean we lose the ability to make some quick changes to the node object in the node_insert and node_edit with the aim of changing the node access records (there's a test demonstrating this here: http://drupal.org/node/1146244#comment-6313310). This can still be done by calling NodeStorageController::save() again but that results in multiple writes to the database. You could make the changes in presave hooks but that might have unintended side effects.

I think it's a good thing we lose that ability too. I don't think we should be using these hooks to make changes to access records (and not the node) as it makes the following code a very bad idea:

$node = node_load(1);
$update = TRUE;
node_access_acquire_grants($node, $update);

Which is basically how node_access_rebuild() works.

So, I completely agree with this change but we need to make sure that it was intentional and didn't just slip in under the radar and is going to cause problems down the line. We probably also need to document the change so that people are aware of it.

Comments

sun’s picture

fago’s picture

Title: Why do node_insert and node_edit hooks now fire after access records are written? » Separate storage operations from reactions
Component: node system » entity system

So, I completely agree with this change but we need to make sure that it was intentional and didn't just slip in under the radar and is going to cause problems down the line. We probably also need to document the change so that people are aware of it.

I agree with that as well. Writing the access records is part of the save operation and thus has to happen before the hook. The hook is for reacting on the save and is documented to happen *after* saving.

Related, modules traditionally used node_insert|update to write their module data. That's bad as it mixes reacting on saves with the save operation. We need to differentiate that. Ideally, I'd like to see going all save operations to entity properties or fields and using the existing hooks for reactions only.
If we don't get there for d8, we should think about introducing two separated hooks, e.g. node_storage_insert vs node_insert.

Imo that the node access problem is just a special case of the general separation issue. Thus, re-titling to the general issue so we can discuss that here.

Dean Reilly’s picture

Imo that the node access problem is just a special case of the general separation issue. Thus, re-titling to the general issue so we can discuss that here.

Good point. I imagine we've seen less issues in other cases because it's possible to play silly beggars with the ordering module hooks fire in and somewhat work around this (whereas the node access records writing is stuck where it is). It would be nice to solve this properly though as you suggest.

Ideally, I'd like to see going all save operations to entity properties or fields and using the existing hooks for reactions only.

Sounds good. I should be able to spend some time this weekend helping out in whatever way I can. In the mean time can you provide a break down of what would need to happen to achieve this? Is it a case of searching through core and looking for hook_insert/update's which need to be refactored to use fields/properties?

If we don't get there for d8, we should think about introducing two separated hooks, e.g. node_storage_insert vs node_insert.

FYI, this is the current proposal for D7 in #1146244: node_access integrity constraint violation on module_invoke_all('node_' . $op, $node);.

Dean Reilly’s picture

Issue summary: View changes

Cleaning up prose.

fago’s picture

Issue summary: View changes
Issue tags: +Entity Field API

As we've the entity field API now in place, I think we should be able to make entity crud hooks reactions only now.

The only thing that's not covered by field methods is adding a multiple load logic, single load can be replaced by a fields getValue() method. Instead of that you can use the PrepareCacheInterface though.
I'm not sure this would have to include dealing with load() at all though, as load() shouldn't be used for reactions anyway.

I've updated the issue summary.

fago’s picture

Issue summary: View changes
Issue tags: +API change
fago’s picture

Issue summary: View changes
effulgentsia’s picture

Priority: Normal » Major

This is a pretty foundational decoupling that affects transaction optimization and many subtle bugs, so raising to major.

andypost’s picture

I'd like to point to other related issue with storage - there's a some logic specific functionality that uses storage to query data (for example #2111357-4: Get rid of comment_count_unpublished() in favor of CommentStorage method) but they are not really swapable because their logic coupled into storage class and probably some manager class.
Probably better to move this to special data-class(service) like suggested in #2068331-32: Convert comment SQL queries to the Entity Query API

effulgentsia’s picture

Priority: Major » Critical
Issue tags: +Approved API change, +beta blocker

Per discussion with catch, this is an approved beta blocker.

catch’s picture

Priority: Critical » Major
Issue tags: -beta blocker

I think I got that wrong. This is only clarifying an existing API limitation, albeit one that's often abused in contrib.

After #221081: Entity cache out of sync and inconsistent when saving/deleting entities those comments are already in core, so downgrading to major for adding extra comments to any other places in the same boat and untagging. If we end up needing to make a small API change, I doubt it'll be high impact enough to warrant blocking beta, but please correct this if it's overly optimistic.

fago’s picture

I think we also need to clarify where postSave() belongs, which decides based on its mood where it belongs right now. Then, we need to change the scope of the transactions to match the docs.

Not sure whether this API changes are fine after beta, but if they are that's fine to me.

Damien Tournoud’s picture

But moreover, it does not work well with our usage of transactions. Right now the transaction includes all those hooks, but any module reacting about an entity being saved should not be included in the transaction of the entity being saved.

I have to disagree with this premise. Most of the time, you do want reactions to be part of the same transaction. I cannot even think about a use case where you would like it to be different.

Crell’s picture

I've had to write hook_node_update implementations that pushed something off to a queue or exit hook several times so as to get out of the transaction. Usually it's when I'm doing something that involves multiple nodes. That almost always results in a race condition that I have to solve using either a queue or hook_exit() (or both).

Unfortunately, I think both use cases do exist.

fago’s picture

ad #13: Imo the entity save should be treated as an atomic operation, while I don't see the reactions being part of that. E.g. if I write a module that creates a log entry for the change and this results in a failure, it shouldn't rollback the whole entity save. The module's reaction itself may be rolled-back, but why would we roll-back the whole save if it was successful?

Surely, there are use-cases where you need to extend the entity save operation. For those you can still add any custom field to the entity which handles that. Or does that appear as too limited?

If it turns out to be too limited, I'd see us introducing two separate hooks, one for module storage operations part of the transaction, and one for reactions not being part of transactions.

Crell’s picture

fago: If we are going to add more responders, I'd suggest that's a good place to start using events (which are well suited to reactions, but not as much to in-place alters).

tstoeckler’s picture

I've also hit the use-case in #14 before, but I think being able to roll back the entire node save from a reaction is a more important feature. logging is one example where rolling back the entire save would be overkill but in terms of derivative data that depends on the node not being able to roll back the node save can lead to data integrity issues. I'm not entirely sure all those use-cases can be covered by using entity fields (although I'm not sure about this). So I think if we do want to support #14 (which I would 100% support) then two hooks/events are the way to go.

jhedstrom’s picture

Status: Active » Postponed (maintainer needs more info)
Issue tags: +Needs issue summary update

Many of the hooks mentioned have been removed. Tagging for IS update.

andypost’s picture

Status: Postponed (maintainer needs more info) » Needs work

NW for summary update

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

fago’s picture

So we have the user-case for reactions being part of the transaction / storage operatoin and for reactions being not. That means, our system should allow for both. However, the current system only allows for reactions being part of the transaction, but we miss the second. I think adding that would be the best solution here.

I'd propose adding an "onChange" event here, which has a $update boolean just as postSave().

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Kingdutch’s picture

My life would've been a lot easier today if this had been implemented :D There are quite a few parallel discussions of similar issues, so adding those as related in the hope that we may add one more hook to rule them all and not fulfill the xkcd.

Kingdutch’s picture

andypost’s picture

Version: 9.5.x-dev » 11.x-dev