mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Do I have to manually compute the changeset of new related entities in onFlush? #5411
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @floriandammeyer on GitHub (Feb 7, 2017).
Originally assigned to: @Ocramius on GitHub.
Inside the
onFlushlistener, we deep copy an existing entity including its relations and the relations of the related entities. The hierarchy looks something like thisSo all entities inside this hierarchy are new for the copied entity. I was hoping that calling
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
PDOExceptionbecause no parameters were bound to thePDOStatement.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?
@Ocramius commented on GitHub (Feb 7, 2017):
$entity_manager->persist($new_parent_entity);is sufficient, since all changes have stateNEW. No change computing needed forNEWentities (they are just queued asINSERToperations)@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
onFlushhandler manually: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#onflushIf I call
persist()andcomputeChangeSet()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 thePDOStatementsto insert the new entities, but it does not set any parameters on the createdPDOStatementso 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
onFlushevent 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
ParentEntityby 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@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 topersist()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 anonFlushlistener, I cannot use theflush()method and instead I have to trigger the computation manually in some way.Calling
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
onFlushhandler. 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.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.
@Ocramius commented on GitHub (Feb 14, 2017):
@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).@floriandammeyer commented on GitHub (Feb 14, 2017):
Yes the relevant relations of the
ParentEntityare marked as 'cascade persist'. If they weren't, my workaround would not work, it would instead throw aNewEntityFoundThroughRelationshipexception 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 toSTATE_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 anonFlushhandler, 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 stateSTATE_NEWwhen you callcomputeChangeSet()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.
@lcobucci commented on GitHub (Feb 14, 2017):
@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.
@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: