Do I have to manually compute the changeset of new related entities in onFlush? #5411

Open
opened 2026-01-22 15:07:05 +01:00 by admin · 8 comments
Owner

Originally created by @floriandammeyer on GitHub (Feb 7, 2017).

Originally assigned to: @Ocramius on GitHub.

Inside the onFlush listener, we deep copy an existing entity including its relations and the relations of the related entities. The hierarchy looks something like this

ParentEntity
    -> SomeChildEntity
    -> OtherChildEntities (ArrayCollection/PersistantCollection)
        -> ChildrenOfChildEntities (ArrayCollection/PersistantCollection)

So all entities inside this hierarchy are new for the copied entity. I was hoping that calling

$entity_manager->persist($new_parent_entity);
$uow->computeChangeSet(
    $entity_manager->getClassMetadata('ParentEntity'),
    $new_parent_entity
);

would be enough to also persist the related entities and their relations, but apparently that doesn't work. The UoW seems to know about the new related entities (i. e. inside the "OtherChildEntities" collection), but the changesets of these entities are empty, so the flush operation always fails with a PDOException because no parameters were bound to the PDOStatement.

Did I do something wrong (the relations are all marked as "cascade=persist" so that shouldn't be it)? I've read in this thread https://github.com/doctrine/doctrine2/issues/5920 that you have to 'take ownership' of your changes and register them with the UoW yourself, so does that mean that I really have to manually compute the changesets of all related entities?

Originally created by @floriandammeyer on GitHub (Feb 7, 2017). Originally assigned to: @Ocramius on GitHub. Inside the `onFlush` listener, we deep copy an existing entity including its relations and the relations of the related entities. The hierarchy looks something like this ``` ParentEntity -> SomeChildEntity -> OtherChildEntities (ArrayCollection/PersistantCollection) -> ChildrenOfChildEntities (ArrayCollection/PersistantCollection) ``` So all entities inside this hierarchy are new for the copied entity. I was hoping that calling ``` $entity_manager->persist($new_parent_entity); $uow->computeChangeSet( $entity_manager->getClassMetadata('ParentEntity'), $new_parent_entity ); ``` would be enough to also persist the related entities and their relations, but apparently that doesn't work. The UoW seems to know about the new related entities (i. e. inside the "OtherChildEntities" collection), but the changesets of these entities are empty, so the flush operation always fails with a `PDOException` because no parameters were bound to the `PDOStatement`. Did I do something wrong (the relations are all marked as "cascade=persist" so that shouldn't be it)? I've read in this thread https://github.com/doctrine/doctrine2/issues/5920 that you have to 'take ownership' of your changes and register them with the UoW yourself, so does that mean that I really have to manually compute the changesets of all related entities?
admin added the Question label 2026-01-22 15:07:05 +01:00
Author
Owner

@Ocramius commented on GitHub (Feb 7, 2017):

$entity_manager->persist($new_parent_entity); is sufficient, since all changes have state NEW. No change computing needed for NEW entities (they are just queued as INSERT operations)

@Ocramius commented on GitHub (Feb 7, 2017): `$entity_manager->persist($new_parent_entity);` is sufficient, since all changes have state `NEW`. No change computing needed for `NEW` entities (they are just queued as `INSERT` operations)
Author
Owner

@floriandammeyer commented on GitHub (Feb 7, 2017):

Unfortunately that doesn't work, and according to the docs, you need to compute the changeset for new entities in the onFlush handler manually: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush

If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).

If I call persist() and computeChangeSet() only for the new parent entity, the UoW tries to insert the new related entities without any parameters, because their changesets are empty. As I said in my initial posting, the UoW does know about the new related entities, it just doesn't compute their changesets. And because of that, the UoW creates the SQL and the PDOStatements to insert the new entities, but it does not set any parameters on the created PDOStatement so the statement throws an exception. This seems to happen for entities that are inside a collection ("OtherChildEntities" in my example) but also for entities that are directly referenced by the parent entity ("SomeChildEntity"). I tried this with the most recent versions of Doctrine inside a ZF2 application (doctrine/doctrine-module v1.2.0, doctrine/doctrine-orm-module v1.1.1 and doctrine/orm v2.5.6).

Again, I am talking about copying the entity inside the onFlush event handler! Under normal circumstances it does work as simple as you described.

Can this be caused by me cloning the related entities instead of creating completely new ones? I copy the ParentEntity by creating a new entity of that type via the new keyword, manually setting all plain attributes of that new entity to the values of the entity that is to be copied and then cloning the related entities via the clone keyword, i. e. $new_entity->setSomeChildEntity(clone $old_entity->getSomeChildEntity()); The __clone method of those entities has been implemented so that Doctrine's internal cloning should also still work. The elements inside the collections are cloned via

$new_entity->setOtherChildEntities(
    $old_entity
        ->getOtherChildEntities()
        ->map(function($item) { return clone $item; })
);
@floriandammeyer commented on GitHub (Feb 7, 2017): Unfortunately that doesn't work, and according to the docs, you need to compute the changeset for new entities in the `onFlush` handler manually: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflush > If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity). If I call `persist()` and `computeChangeSet()` only for the new parent entity, the UoW tries to insert the new related entities without any parameters, because their changesets are empty. As I said in my initial posting, the UoW does know about the new related entities, it just doesn't compute their changesets. And because of that, the UoW creates the SQL and the `PDOStatements` to insert the new entities, but it does not set any parameters on the created `PDOStatement` so the statement throws an exception. This seems to happen for entities that are inside a collection ("OtherChildEntities" in my example) but also for entities that are directly referenced by the parent entity ("SomeChildEntity"). I tried this with the most recent versions of Doctrine inside a ZF2 application (doctrine/doctrine-module v1.2.0, doctrine/doctrine-orm-module v1.1.1 and doctrine/orm v2.5.6). **Again, I am talking about copying the entity inside the `onFlush` event handler!** Under normal circumstances it does work as simple as you described. Can this be caused by me cloning the related entities instead of creating completely new ones? I copy the `ParentEntity` by creating a new entity of that type via the new keyword, manually setting all plain attributes of that new entity to the values of the entity that is to be copied and then cloning the related entities via the clone keyword, i. e. `$new_entity->setSomeChildEntity(clone $old_entity->getSomeChildEntity());` The __clone method of those entities has been implemented so that Doctrine's internal cloning should also still work. The elements inside the collections are cloned via ``` $new_entity->setOtherChildEntities( $old_entity ->getOtherChildEntities() ->map(function($item) { return clone $item; }) ); ```
Author
Owner

@floriandammeyer commented on GitHub (Feb 14, 2017):

I guess I figured out what the problem is.

When I call $entity_manager->persist($new_parent_entity);, the new entity including all of its related entities are registered with the UnitOfWork and their state is being changed from STATE_NEW to STATE_MANAGED, right here UnitOfWork.php#L909. So all new entities are now managed. However, the call to persist() does not yet trigger changeset computation. Usually the changeset computation of all managed entities would be triggered by $entity_manager->flush() but since I am inside an onFlush listener, I cannot use the flush() method and instead I have to trigger the computation manually in some way.

Calling

$uow->computeChangeSet(
    $entity_manager->getClassMetadata('ParentEntity'),
    $new_parent_entity
);

triggers the changeset computation for my new entity. The computation would be cascaded to the related entities if they still had the STATE_NEW, see here: UnitOfWork.php#L850. However the related entities have the STATE_MANAGED, so the UoW thinks their changesets are being calculated elsewhere so nothing happens: UnitOfWork.php#L873

A workaround is reordering the code in the onFlush handler. Instead of creating the new parent entity, populating its values, persisting and then computing the changeset, you can simply persist the new parent entity immediately after instantiation so no references are set yet. When you later compute the changeset, the related entities will have STATE_NEW instead of STATE_MANAGED so their changesets are automatically computed.

// Does not work as expected:
$new_parent_entity = new ParentEntity();
$new_parent_entity->setSomeChildEntity( clone $old_entity->getSomeChildEntity() );

$entity_manager->persist($new_parent_entity);
$uow->computeChangeSet(
    $entity_manager->getClassMetadata('ParentEntity'),
    $new_parent_entity
);

// DOES work as expected:
$new_parent_entity = new ParentEntity();
$entity_manager->persist($new_parent_entity);

$new_parent_entity->setSomeChildEntity( clone $old_entity->getSomeChildEntity() );

$uow->computeChangeSet(
    $entity_manager->getClassMetadata('ParentEntity'),
    $new_parent_entity
);

Not knowing about this quirk has cost me a lot of time... I guess mentioning it in the docs would be easier than changing the internals of the UoW, if changing the UoW is an option at all.

@floriandammeyer commented on GitHub (Feb 14, 2017): I guess I figured out what the problem is. When I call `$entity_manager->persist($new_parent_entity);`, the new entity including all of its related entities are registered with the UnitOfWork and their state is being changed from STATE_NEW to STATE_MANAGED, right here [UnitOfWork.php#L909](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L909). So all new entities are now managed. However, the call to `persist()` does not yet trigger changeset computation. Usually the changeset computation of all managed entities would be triggered by `$entity_manager->flush()` **but since I am inside an `onFlush` listener, I cannot use the `flush()` method** and instead I have to trigger the computation manually in some way. Calling ```php $uow->computeChangeSet( $entity_manager->getClassMetadata('ParentEntity'), $new_parent_entity ); ``` triggers the changeset computation for my new entity. The computation would be cascaded to the related entities if they still had the STATE_NEW, see here: [UnitOfWork.php#L850](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L850). However the related entities have the STATE_MANAGED, so the UoW thinks their changesets are being calculated elsewhere so nothing happens: [UnitOfWork.php#L873](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L873) A workaround is reordering the code in the `onFlush` handler. Instead of creating the new parent entity, populating its values, persisting and then computing the changeset, you can simply persist the new parent entity immediately after instantiation so no references are set yet. When you later compute the changeset, the related entities will have STATE_NEW instead of STATE_MANAGED so their changesets are automatically computed. ```php // Does not work as expected: $new_parent_entity = new ParentEntity(); $new_parent_entity->setSomeChildEntity( clone $old_entity->getSomeChildEntity() ); $entity_manager->persist($new_parent_entity); $uow->computeChangeSet( $entity_manager->getClassMetadata('ParentEntity'), $new_parent_entity ); // DOES work as expected: $new_parent_entity = new ParentEntity(); $entity_manager->persist($new_parent_entity); $new_parent_entity->setSomeChildEntity( clone $old_entity->getSomeChildEntity() ); $uow->computeChangeSet( $entity_manager->getClassMetadata('ParentEntity'), $new_parent_entity ); ``` Not knowing about this quirk has cost me a lot of time... I guess mentioning it in the docs would be easier than changing the internals of the UoW, if changing the UoW is an option at all.
Author
Owner

@Ocramius commented on GitHub (Feb 14, 2017):

Not knowing about this quirk has cost me a lot of time... I guess mentioning it in the docs would be easier than changing the internals of the UoW, if changing the UoW is an option at all.

@floriandammeyer changing UoW internals is feasible if the changes are BC compliant.

I must ask: is there a cascade persist set on ParentEntity? Because I'd expect the first scenario to work in that case (without needing to recompute changes).

@Ocramius commented on GitHub (Feb 14, 2017): > Not knowing about this quirk has cost me a lot of time... I guess mentioning it in the docs would be easier than changing the internals of the UoW, if changing the UoW is an option at all. @floriandammeyer changing UoW internals is feasible if the changes are BC compliant. I must ask: is there a cascade persist set on `ParentEntity`? Because I'd expect the first scenario to work in that case (without needing to recompute changes).
Author
Owner

@floriandammeyer commented on GitHub (Feb 14, 2017):

Yes the relevant relations of the ParentEntity are marked as 'cascade persist'. If they weren't, my workaround would not work, it would instead throw a NewEntityFoundThroughRelationship exception in UnitOfWork.php#L852.

As mentioned in my previous comment: if you follow the code execution with a debugger you can see that this problem is caused by the states of the associated entities. $em->persist($new_entity) sets the state of all associated entities to STATE_MANAGED. The UnitOfWork does not compute the changeset of associated entities with this state in UnitOfWork.php#L873, because it expects the changesets to be computed elsewhere. However because we are inside an onFlush handler, this expected computation does not happen if you don't do it manually. That's why the first scenario does not work unless you manually compute the changesets of all associated entities.

In the seconde scenario the associated entities are not yet set when persist() is called, so the associated entities have the state STATE_NEW when you call computeChangeSet() later, so the UnitOfWork computes the changesets itself in UnitOfWork.php#L850.

The problem might be clearer with a full example, I will put something together and push it to my GitHub account when I have the time.

@floriandammeyer commented on GitHub (Feb 14, 2017): Yes the relevant relations of the `ParentEntity` are marked as 'cascade persist'. If they weren't, my workaround would not work, it would instead throw a `NewEntityFoundThroughRelationship` exception in [UnitOfWork.php#L852](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L852). As mentioned in my previous comment: if you follow the code execution with a debugger you can see that this problem is caused by the states of the associated entities. `$em->persist($new_entity)` sets the state of all associated entities to `STATE_MANAGED`. The UnitOfWork does not compute the changeset of associated entities with this state in [UnitOfWork.php#L873](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L873), because it expects the changesets to be computed elsewhere. However because we are inside an `onFlush` handler, this expected computation does not happen if you don't do it manually. That's why the first scenario does not work unless you manually compute the changesets of all associated entities. In the seconde scenario the associated entities are not yet set when `persist()` is called, so the associated entities have the state `STATE_NEW` when you call `computeChangeSet()` later, so the UnitOfWork computes the changesets itself in [UnitOfWork.php#L850](https://github.com/doctrine/doctrine2/blob/v2.5.6/lib/Doctrine/ORM/UnitOfWork.php#L850). The problem might be clearer with a full example, I will put something together and push it to my GitHub account when I have the time.
Author
Owner

@lcobucci commented on GitHub (Feb 14, 2017):


> ~~~If you create and persist a new entity in onFlush, then calling `EntityManager#persist()` is not enough. You have to execute an additional call to `$unitOfWork->computeChangeSet($classMetadata, $entity)`.~~~

Never mind I misread some stuff, sorry.

@floriandammeyer can you try to call `UnitOfWork#computeScheduleInsertsChangeSets()` before of `UnitOfWork#computeChangeSet($metadata, $entity)`?
@lcobucci commented on GitHub (Feb 14, 2017): ~~~@floriandammeyer @Ocramius aren't those issues documented on http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/events.html#onflush ?~~~ > ~~~If you create and persist a new entity in onFlush, then calling `EntityManager#persist()` is not enough. You have to execute an additional call to `$unitOfWork->computeChangeSet($classMetadata, $entity)`.~~~ Never mind I misread some stuff, sorry. @floriandammeyer can you try to call `UnitOfWork#computeScheduleInsertsChangeSets()` before of `UnitOfWork#computeChangeSet($metadata, $entity)`?
Author
Owner

@akomm commented on GitHub (Apr 21, 2017):

Came across the same issue. I have a similar situation. There is a case when reordering only partially helps.

There are entity types root, parent, children (1:1:n). I have cascade persist and orphan removal down the chain root -> parent -> children. Parent is nullable. The children are generated on flush and assigned to parent when it is scheduled for insertion.

The reorder-solution works when parent is assigned for the first time (from null to reference). But it fails when you replace referenced parent by another new one. The generated query attempts to insert a new parent with the id of the old one.

This happens because root has an API for parent creation (defensive API) and the persist is applied on root.

When you persist parent & computeChangeSet on flush event, parent gets marked as new because it is a new instance (its replaced, not altered), but root still has its old parent id tracked from initial persist. The first cause an insert decision and the second cause the query to have id parameter from old parent.

I'v tried to compute/recompute changes from root but this brings me back to the initial problem of cascaded changes not being executed.

@akomm commented on GitHub (Apr 21, 2017): Came across the same issue. I have a similar situation. There is a case when reordering only partially helps. There are entity types root, parent, children (1:1:n). I have cascade persist and orphan removal down the chain root -> parent -> children. Parent is nullable. The children are generated on flush and assigned to parent when it is scheduled for insertion. The reorder-solution works when parent is assigned for the first time (from null to reference). But it fails when you replace referenced parent by another new one. The generated query attempts to insert a new parent with the id of the old one. This happens because root has an API for parent creation (defensive API) and the persist is applied on root. When you persist parent & computeChangeSet on flush event, parent gets marked as new because it is a new instance (its replaced, not altered), but root still has its old parent id tracked from initial persist. The first cause an insert decision and the second cause the query to have id parameter from old parent. I'v tried to compute/recompute changes from root but this brings me back to the initial problem of cascaded changes not being executed.
Author
Owner

@klobastov commented on GitHub (Oct 7, 2023):

Similar problem, when trying to apply changes to an entity that has been "restored" by reflection. I.e. it is really a different entity, but the data in it is the same.

Example solution:

// Repository code 
$uow = $this->em->getUnitOfWork();
$originalEntityId = $uow->getEntityIdentifier($originalEntity);
$originalEntityData = $uow->getOriginalEntityData($originalEntity);

$uow->clear(self::ENTITY_CLASS);

$uow->registerManaged($restoredEntity, $originalEntityId, $originalEntityData);
$classMetadata = $this->em->getClassMetadata(self::ENTITY_CLASS);
$uow->recomputeSingleEntityChangeSet($classMetadata, $restored);
@klobastov commented on GitHub (Oct 7, 2023): Similar problem, when trying to apply changes to an entity that has been "restored" by reflection. I.e. it is really a different entity, but the data in it is the same. Example solution: ``` // Repository code $uow = $this->em->getUnitOfWork(); $originalEntityId = $uow->getEntityIdentifier($originalEntity); $originalEntityData = $uow->getOriginalEntityData($originalEntity); $uow->clear(self::ENTITY_CLASS); $uow->registerManaged($restoredEntity, $originalEntityId, $originalEntityData); $classMetadata = $this->em->getClassMetadata(self::ENTITY_CLASS); $uow->recomputeSingleEntityChangeSet($classMetadata, $restored); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#5411