computeChangeSet of class UnitOfWork rewrites not flushed changes
Bug Report
| Q | A |
|---|---|
| BC Break | no |
| Version | 2.7.0 but I guess reproducibly on 3.0 |
Summary
Using method computeChangeSets of class UnitOfWork, Im updating list of entities that going to be inserted/changed/deleted. At this point my entities still not flushed to database, but persisted to entityManager. After this, if I try to edit affected entity and flush - UnitOfWork runs computeChangeSets again and rewrites old change list ($this->entityChangeSets[$oid] in UnitOfWork). During flushing there are no enough data, so Im getting error like: SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens, because old data was lost.
Current behavior
method computeChangeSets of class UnitOfWork rewrites old change list, even if entity was not flushed into db
How to reproduce
Create object of entity, persist to entityManager, call method computeChangeSets of class UnitOfWork, change something in object, call flush method of entityManager
Expected behavior
method computeChangeSets is public, so UnitOfWork must take it into account, because it can be used in client code. Method should not rewrites, but merge old and new entity change list.
Possible solution:
https://github.com/doctrine/orm/blob/3.0.x/lib/Doctrine/ORM/UnitOfWork.php#L715 Change:
if ($changeSet) {
$this->entityChangeSets[$oid] = $changeSet;
$this->originalEntityData[$oid] = $actualData;
$this->entityUpdates[$oid] = $entity;
}
To:
if ($changeSet) {
if (isset($this->entityChangeSets[$oid])) {
$newChangeSet = array_merge($this->entityChangeSets[$oid], $changeSet);
} else {
$newChangeSet = $changeSet;
}
$this->entityChangeSets[$oid] = $newChangeSet;
$this->originalEntityData[$oid] = $actualData;
$this->entityUpdates[$oid] = $entity;
}
I am not really sure what the intended use case for that method is. A Google search turned up this one: https://github.com/doctrine/orm/issues/5945
Can you provide example code to illustrate your use case?
I used this method to get list of entities that gonna be persisted/removed/updated, like this:
$this->entityManager->computeChangeSets(); $unitOfWork = $this->entityManager->getUnitOfWork();
$toInsert = $unitOfWork->getScheduledEntityInsertions(); $toUpdate = $unitOfWork->getScheduledEntityUpdates(); $toDelete = $unitOfWork->getScheduledEntityDeletions(); ...
All is working properly until I dont want to change this entities before flushing
I have the "same" problem (I have no exceptions). There is a simple example :
$uof = $em->getUnitOfWork();
$event = $eventRepository->find(70);
$event->setTitle('a new title');
$uof->computeChangeSets();
$event->setDescription('new description');
$em->flush(); // title is not updated on database !
So, the "problem" is if the entity change again after the custom call of "computeChangeSets", the entitymanager lost previous changes. I think the possible solution of @ioSeoGio seems ok, I don't see potential bug.
My use case is, I need to get the changeset of an entity on a controller (or whatever, I mean not in a preUpdateEvent), and if not empty, add a relation to the entity. Then the code after may update it again, and I don't care. Please don't ask more, it's simplified here.
If you use $uow->computeChangeSet(...) instead of $uow->computeChangeSets() you can call $uow->recomputeSingleEntityChangeSet(...) before flushing.
$uow = $em->getUnitOfWork();
$event = $eventRepository->find(70);
$event->setTitle('a new title');
$eventClassMetadata = $em->getClassMetadata(Event::class);
$uof->computeChangeSet($eventClassMetadata, $event);
//... do something here, maybe return if event isScheduledForUpdate is false
$event->setDescription('new description');
$uow->recomputeSingleEntityChangeSet($eventClassMetadata, $event)
$em->flush();
I also ran into this issue today. I know we're basically abusing the unit of work, but I still feel like it would be nice to be able to get the changeSet without having to resort to a "solution" like using recomputeSingleEntityChangeSet. Using the latter, you're basically moving the problem to other potential code that wants to update the entity later.
If we'd have a function like discardComputedChangeSets or discardComputedEntityChangeSet, we could nicely clean the unit of work and pretend like nothing happened :) (Not sure how feasible that is though.)
(Edit: for now I just do a single query that fetches the persisted value, and compare that to what's in the entity to avoid this alltogether.)
I'm having the same problem as well. I'm currently resorting to the UnitOfWork to determine what the changes would be if I flushed my work and determine further steps based on that.
For this use case, an API allowing me to "virtually" compute a change set without it having an effect on the database or unit of work would be ideal. So in a similar way of @Qronicle 's solution (but kind of the other way around) this would incorporate a UnitOfWork::computeChangeSets and UnitOfWork::applyChangeSets method, where to former one returns a change set and the latter one takes one and applies it to the unit of work.