Doctrine creates new row when editing entity from cache - php

I have retrieved a simple (no joins) serialised entity from a Redis cache.
When I edit this entity and call persist() and flush() on the entity manager, a new row is inserted instead of updating the existing entity.
The Doctrine documentation only seems to mention calling merge() with the entity but that fetches the item from the database which defeats the purpose of caching it.
How can I make the entity manager track this restored entity?
Thanks

Unfortunately, you do need to call merge() - this is just so that the EntityManager is properly aware of the entity. If you serialise the entity and then load it back up in a new session then the EntityManager now only knows the type and the attributes - it doesn't know if it has been previously saved to the database etc, which is why it just assumes it is new and creates a new row unless merged.
The EntityManager has an internal list of what's been saved, what's pending etc; it needs all this info to correctly do it's job - these details are not stored in the entities themselves. An unserialised entity read from cache is considered "unmanaged".
From the Doctrine docs on this:
In Doctrine an entity objects has to be “managed” by an EntityManager
to be updateable. Entities saved into the session are not managed in
the next request anymore. This means that you have to register these
entities with an EntityManager again if you want to change them or use
them as part of references between other entities. You can achieve
this by calling EntityManager#merge().
It doesn't completely defeat the purpose of caching it, because really you only need to merge at the point that you want to save back to the database.

Related

Unique error persisting entity updates to db with Doctrine

I'm using Doctrine in my Symfony project to manage the persisting layer in my application.
Recently, I've been having some issues when persisting changes from my entities to my database. The problem I've been having is that when I update an entity and save it to my database, sometimes the EntityManager treats my entity as a new object, so instead of performing an update operation, it performs an insert operation, thus causing a unique exception error in my database.
As the docs say, when updating an object you should only perform these steps:
fetch the object from Doctrine
modify the object
(optional) call persist() on the entity manager
call flush() on the entity manager
Note I added (optional) to the persist() call because, as the docs say, it isn't necessary since Doctrine is already watching the object for changes
Now that things are explained, this is the work I do in my code:
$myEntity = $this->myEntityRepository->byId($id);
// make some changes to the entity
$myEntity->setSomething('something');
$this->myEntityRepository->save($entity);
Where the save() operation in my repository is as follows:
$this->entityManager->persist($entity);
$this->entityManager->flush();
And the byId() operation:
return $this->entityManager->getRepository()->find($id);
As I said, the persist operation should only be called when persisting new entities, but since Doctrine can differentiate between an already managed entity and a new one, it should be no problem. If I didn't call the persist() method, instead of executing and insert operation and cause a unique violation, it would literally do nothing as it wouldn't detect any changes to my operations.
The reason I always use the persist() method is because the save() operation in my repository is used with both new entities and updates to existing entities.
As I've seen in another answer, calling the merge() operation instead of persist() should solve the problem, but I don't see it right because I think it's just a "dirty" solution, plus the method is being deprecated in future versions of Doctrine.
So, what am I missing here? Why sometimes I get a unique error when running the above code? I only have one connection and one entity manager configured in my application.
I'd like to add that the only occurrences of this problem are found in code executed in consumers (async events), not in the API itself, but whenever I receive a new event, a new and fresh connection to the database is created to ensure I don't have overlapping problems with the entity manager used in some previous event.
When talking about consumers I mean that an event is published via RabbitMQ (the event contains the ID of the entity) and then it gets consumed in a separate process from the API, fetching the entity directly with the entity manager repository.
My guess is that between the line where I get my entity from the repository (i.e. I use the find() method) and the line where I save it into the database (i.e. I use the flush() method), the entity manager somehow removes the entity from its UnitOfWork so it treats it as a new entity instead of a managed one.
An issue I recently found in some code was where an entity was being passed - via a RabbitMq message and the entity created from scratch, including the id -- $x = new Entity(); $x->setId($data['id');$x->setName($data['name'); # ... etc).
Having an id set does not make the entity 'managed' by Doctrine however - only reading the original record (from the DB) and then updating it. Persisting $x as above will create a new record - and ignoring the id that was set.
I would ensure (perhaps with some assertions on the record that you are fetching that must pass - or die with an error, at least for now) so that know that you are always dealing with a known entity (I'm also presuming that your ->byId($id) call is just a ->find($id) internally).

How to perist only the first level of an entities hierarchy with Doctrine?

I'm migrating the DBAL of a ZF3 application to Doctrine and want to go ahead step by step. Currently I'm using a hierarchy of Mapper objects. Each entity in the like FooEntity hierarchy has an according FooMapper. Saving of nested entities is performed by nested Mappers. Every Mappers saves its entity with Zend\Db\Sql\Insert or Zend\Db\Sql\Update and calls the proper Mappers for the sub-entities like BarMapper for BarEntity.
Now, before I start with Doctrine's convenience features like cascade={"persist"}, I want to keep the Mapper's hierarchy and just to perform the saving of the top level of the nested entity with persist(...) & flush().
But when I try it
public function save(AbstractDataObject $dataObject)
{
$newLogicalConnection = $this->logicalConnectionMapper->save($dataObject->getLogicalConnection());
$newUser = $this->userMapper->save($dataObject->getUser());
$dataObject->setLogicalConnection($this->entityManager->find(LogicalConnection::class, $newLogicalConnection->getId()));
$dataObject->setUser($this->entityManager->find(User::class, $newUser->getId()));
$this->entityManager->persist($dataObject);
$this->entityManager->flush();
return $dataObject;
}
I get an error
A new entity was found through the relationship 'MyNamespace\DataObject\AbstractEndpoint#externalServer' that was not configured to cascade persist operations for entity: MyNamespace\DataObject\ExternalServer#000000006098ccff0000000068c23676. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'MyNamespace\DataObject\ExternalServer#__toString()' to get a clue.
So, Doctrine seems to try saving the whole entity with its sub-entities, and this attempt fails on one of the lower levels. But why? I have not activated any cascade options and expect Doctrine to save only the top level.
Why does Doctrine try to save the whole entity and not only the top level? And how to get it saving only the top level of the given entity?
You get this error because you have a new entity (not persisted yet) in AbstractEndpoint->externalServer and this field is not annotated as cascade={"persist"}
In other words you have just created a new entity ExternalServer and did not persisted it and added it as a relation to AbstractEndpoint->externalServer entity which is not annotated as cascade={"persist"}
So Doctrine ends up having this new entity and does not know what to do with it. In order not to lost any data this exception is raised.
To fix this you can do two things:
Add $this->entityManager->persist($externalServer); right after you create ExternalServer entity
Annotate AbstractEndpoint->externalServer with cascade={"persist"}. Which you don't want to do because you want only top level entity to be saved to DB so you need to persist it manually or DO NOT add it is a relation.
And now answering your question:
But why? I have not activated any cascade options and expect Doctrine to save only the top level.
Somehow through relations in your object model Doctrine goes down to ExternalServer entity and finds it in unpersisted state. You can not save only top level of object hierarchy with link to unexisting record in relational database. If you do not want Doctrine to do it for you - you must handle this situations by yourself or remove not persisted entities from relations

Inserting data with Doctrine - Repository Vs Entity

I've realized there is a difference between a Doctrine repository and Doctrine entity.
I'm trying to implement simple CRUD actions on a table and was injecting a default Doctrine repository into my controller (without injecting an entity).
For the "Update" action I would first ->find($id) for the record to update and it would return an instance of the entity for me to bind to my form object.
For the "Create" action I realized I can't ->find($id) a record to insert (since it doesn't exist) in order to retrieve an instance of the entity for me to bind to my form object.
Is there is an alternate way to insert data using Doctrine without an instance of an entity? Or is there a way to retrieve an instance of the entity from the repository so I can ->bind() it to the form? If the answer to both are no, then I imagine my only options are to inject an instance of the entity to my controller, or to use a custom repository which contains a method which would return an entity to use in the ->bind() for insertion.
My guess would be to define a custom repository which has a method which retrieves an empty entity instance for use in insertion. Is this assumption correct?
As pointed by #Crisp in comments, Entities are no more than PHP classes, same for Repositories.
The two are differentiated by their respective role.
You'll never implicitly create a new instance of a Repository because doctrine do it for you across DependencyInjection principles (Service, Factory, ...).
To create a new database entry, you must create a new instance of the corresponding entity, then store it using EntityManager::persist and EntityManager::flush methods.
Reuse the same instance of an entity would not give you any benefit, nor make any difference in your project's maintainability.
The entity class itself will never be broken/changed, only instances of them are created, renamed, moved, deleted.
These instances represents your database entries, this is the primary interest of use an ORM.

Handling relational tables as entities with Doctrine

I need some help dealing with a relational table that is an entity due to the existence of an additional property.
Here is a gist of the entities in question: https://gist.github.com/chasepeeler/efd7efd890c58eafb81f
Do I have something configured wrong that is forcing me to do the flush in controller.php line 15?
I've also tried just updating the rank attribute of the queueItem record in the Queue::queueItems collection, but when I do that, it doesn't even save the changes to the database.
$queueItems->clear() does the same thing as clearQueueItems, but one time.
And if you want to override current queue state, you should just implement and call setQueueItems(ArrayCollection $queueItemList) method.
UnitOfWork will compute your changes to insert and remove new/deleted items.
Every OneToMany annotated field should implement setItems, addItem and removeItem methods, where Item is related entity name.
Your sortQueue method shouldn't persist and commit changes into database.
It should only return a sorted Collection.
Maybe I didn't get that, it's hard to say what you want to achieve, controller's code says me nothing.

Doctrine refresh copy of entity

I have a CustomerAccount entity. After that entity has had changes made to it via a form, but before the entity has been persisted to the database, I need to fetch a new copy of the same CustomerAccount with the entity as it currently exists in the database. The reason I need to do this is I want to fire off a changed event with both the old and new data in my service.
One hack I used was $oldAccount = unserialize(serialize($account)); and passing the old into my service, but thats really hackish.
What I would really like to do is have Doctrine pull back a copy of the original entity (while keeping the changes to the new version).
Is this even possible?
Update
It appears what I really want to do is ultimately impossible at this time with the way Doctrine is architected.
Update 2
I added the solution I ultimately ended up using at the bottom. I'm not completely happy with it because it feels hackish, but it gets the job done and allows me to move on.
It depends.
I mean, Doctrine2 use the IdentityMap that prevents you "accidentally" query the db for the same object over and over again into the same request. The only way to force doctrine fetch entity object again is to detach the entity from the entity manager and request entity again.
This, however, could lead to some strange behaviour that could "slip" out of your control:
you can't persist again a detached object
if you try to persist an object that is related ("linked") to your detached entity you will run into troubles (and sometimes is very difficult to debug)
So, why don't you try with php built-in clone function? Maybe is more suitable for you and could save you from a lot of debugging
Code example:
$em = $this->getDoctrine()->getManager();
$fetched_entity = $em->findOnById(12);
$cloned_entity = clone $fetched_entity;
//and so on ...
Here is the ultimate solution I ended up using. I created a duplicate entity manager in my config.yml and retrieved a second copy of the entity from the duplicate entity manager. Because I won't make any changes to the entity retrieved by the duplicate entity manager, this solution was the best for my use case.

Categories