Lost Update when using Deferred Explicit change tracking, Remove followed by Persist. #6518

Closed
opened 2026-01-22 15:34:25 +01:00 by admin · 1 comment
Owner

Originally created by @skurfuerst on GitHub (Aug 7, 2020).

... That's hard to describe, I'll try nevertheless :-)

Background

  • We use Deferred Explicit as change tracking policy; and we have a single entity which is enough to reproduce the issue.
  • The entity is persisted in the database already, and we have loaded it as $entity.

How to reproduce

  • call $entityManager->remove($entity);
  • modify the entity $entity->setFoo('bar');
  • call $entityManager->persist($entity);
  • call $entityManager->flush();
  • EXPECTED: The entity should be updated in the database.
  • ACTUAL: the entity is not updated.

Root Cause Analysis:

  • call $entityManager->remove($entity);
    • Internally, this calls UnitOfWork::doRemove(), where the entity is marked with UnitOfWork::STATE_REMOVED.
  • modify the entity $entity->setFoo('bar');
  • call $entityManager->persist($entity);
    • Internally, this calls UnitOfWork::doPersist().
    • There, the entity is marked as UnitOfWork::STATE_MANAGED again (it was marked as removed beforehand).
    • NOTE: Doctrine does NOT call UnitOfWork::scheduleForDirtyCheck(), which it would call if the entity was already
      in STATE_MANAGED. We rely on this scheduleForDirtyCheck to be called, because we use DEFERRED_EXPLICIT as change tracking policy.
  • call $entityManager->flush();
    • the entity does not appear in the changed entities.
  • EXPECTED: The entity should be updated in the database.
  • ACTUAL: the entity is not updated.

Suggested fix:

in UnitOfWork::doPersist

private function doPersist($entity, array &$visited) {
  // ... 
     switch ($entityState) {
            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

            case self::STATE_NEW:
                $this->persistNew($class, $entity);
                break;

            case self::STATE_REMOVED:
                // Entity becomes managed again
                unset($this->entityDeletions[$oid]);
                $this->addToIdentityMap($entity);

                $this->entityStates[$oid] = self::STATE_MANAGED;

// BEGIN CHANGE
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
// END CHANGE

                break;

            case self::STATE_DETACHED:
                // Can actually not happen right now since we assume STATE_NEW.
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");

            default:
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
        }
    // ... 
}

Note: this issue appeared as part of https://github.com/neos/neos-development-collection/pull/3015

Thanks for your great work :)
Sebastian

Originally created by @skurfuerst on GitHub (Aug 7, 2020). ... That's hard to describe, I'll try nevertheless :-) ## Background - We use Deferred Explicit as change tracking policy; and we have a single entity which is enough to reproduce the issue. - The entity is persisted in the database already, and we have loaded it as `$entity`. ## How to reproduce - call `$entityManager->remove($entity);` - modify the entity `$entity->setFoo('bar');` - call `$entityManager->persist($entity);` - call `$entityManager->flush();` - EXPECTED: The entity should be updated in the database. - ACTUAL: the entity is not updated. ## Root Cause Analysis: - call `$entityManager->remove($entity);` - Internally, this calls `UnitOfWork::doRemove()`, where the entity is marked with `UnitOfWork::STATE_REMOVED`. - modify the entity `$entity->setFoo('bar');` - call `$entityManager->persist($entity);` - Internally, this calls `UnitOfWork::doPersist()`. - There, the entity is marked as `UnitOfWork::STATE_MANAGED` again (it was marked as removed beforehand). - **NOTE:** Doctrine does NOT call `UnitOfWork::scheduleForDirtyCheck()`, which it would call if the entity was already in `STATE_MANAGED`. We rely on this `scheduleForDirtyCheck` to be called, because we use DEFERRED_EXPLICIT as change tracking policy. - call `$entityManager->flush();` - the entity does not appear in the changed entities. - EXPECTED: The entity should be updated in the database. - ACTUAL: the entity is not updated. ## Suggested fix: in `UnitOfWork::doPersist` ``` private function doPersist($entity, array &$visited) { // ... switch ($entityState) { case self::STATE_MANAGED: // Nothing to do, except if policy is "deferred explicit" if ($class->isChangeTrackingDeferredExplicit()) { $this->scheduleForDirtyCheck($entity); } break; case self::STATE_NEW: $this->persistNew($class, $entity); break; case self::STATE_REMOVED: // Entity becomes managed again unset($this->entityDeletions[$oid]); $this->addToIdentityMap($entity); $this->entityStates[$oid] = self::STATE_MANAGED; // BEGIN CHANGE // Nothing to do, except if policy is "deferred explicit" if ($class->isChangeTrackingDeferredExplicit()) { $this->scheduleForDirtyCheck($entity); } // END CHANGE break; case self::STATE_DETACHED: // Can actually not happen right now since we assume STATE_NEW. throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted"); default: throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity)); } // ... } ``` Note: this issue appeared as part of https://github.com/neos/neos-development-collection/pull/3015 Thanks for your great work :) Sebastian
admin closed this issue 2026-01-22 15:34:25 +01:00
Author
Owner

@beberlei commented on GitHub (Sep 13, 2020):

@skurfuerst Valid report, please make it a pull request against the 2.7 branch.

@beberlei commented on GitHub (Sep 13, 2020): @skurfuerst Valid report, please make it a pull request against the 2.7 branch.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6518