21

I am working an Symfony 2.8 based web app project which currently uses Doctrine 2. The project is basically a simple ToDo list application which can be synced with a mobile app (iOS/Android).

While reading the Update notes of Doctrine 3 I discovered, that EntityManager::merge will no longer be supported.

An alternative to EntityManager#merge() is not provided by ORM 3.0, since the merging semantics should be part of the business domain rather than the persistence domain of an application. If your application relies heavily on CRUD-alike interactions and/or PATCH restful operations, you should look at alternatives such as JMSSerializer.

I am not sure what is the best/correct way to replace EntityManager::merge?

Where do I use merge:

During the sync of the mobile apps with the web app the data is transferred as serialized JSON which is than de-serialized by JMSSerializer to an entity object. When the web app receives a ToDoEntry object this way, it can be a new ToDo-Entry (not known in the web app yet) or an updated existing entry. Either way, the received object is not managed by the EntityManager. Thus $em->persist($receivedObject) will always try to insert a new object. This will fail (due to the unique constraint of the id) if the ToDo-Entry already exists in the web app and needs to be updated.

Instead $em->merge($receivedObject) is used which automatically checks wether an insert or update is required.

Hot wo solve this?

Of course I could check for every received objects if an entity with the same ID already exists. In this case could load the existing object and update its properties manually. However this would be very cumbersome. The real project of course uses many different entities and each entity type/class would need its own handling to check which properties needs to be updated. Isn't there a better solution?

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225
  • 1
    Checking wether an object exist or not and doing things accordingly isn't cumbersome in my opinion and should really by done by yourself in your business logic. Thats nothing unusual. – Jim Panse Jul 06 '18 at 10:58
  • The checking itself it not the problem, but updating the existing entities is. Each entity type would need its own method to compare/override the existing properties with the one of the received object. Each time a property is added to an entity one also has to update the sync/update code. No magic of course but a lot more work than a simple `$em->merge(...)` call. Is this really the right way to go? – Andrei Herford Jul 06 '18 at 11:25
  • In my opinion you don't need to change your method every time a property is removed or added ... just write a flexible merge method which will iterate over your properties and do whatever you want with new/old values. The point they removed this function is that nobody knows your requirement and how the merge function should behave. What if both (new and old) has the property filled? Which one will win? That are things you should decide with regard to your requirements and is part of the business logic and not the responsibility of the database. – Jim Panse Jul 06 '18 at 11:43
  • Assume the entity `ToDoEntry` gets a new property `person` with `setPerson(...)` and `getPerson()`. How could an existing `merge` method handle this property without knowing it exists? I would have to add something like `$existingEntity->setPerson($receivedEntity->getPerson())`, or am I missing something? – Andrei Herford Jul 06 '18 at 11:52
  • You could use reflection for that. http://php.net/manual/en/reflectionclass.getproperties.php ... – Jim Panse Jul 06 '18 at 12:18
  • If you like the behavior of $em->merge just copy paste the function to your codebase – marcodl Dec 13 '20 at 10:11

3 Answers3

2

You can try to use registerManaged() method of Doctrine\ORM\UnitOfWork.

// $this->em <--- Doctrine Entity Manager
// $entity <--- detached Entity (and we know that this entity already exists in DB for example)

$id = [$entity->getId()]; //array
$data = $entity->toArray(); //array

$this->em->getUnitOfWork()->registerManaged($entity, $id, $data);

Of course, You can check the state of Your Entity using getEntityState() of Doctrine\ORM\UnitOfWork before/after perfoming needed actions.

$this->eM->getUnitOfWork()->getEntityState($entity, $assert = 3)

$assert <-- This parameter can be set to improve performance of entity state detection by potentially avoiding a database lookup if the distinction between NEW and DETACHED is either known or does not matter for the caller of the method.

  • 2
    function registerManaged is marked as @internal and should not be used, there is no other way? – patryno Feb 18 '21 at 09:08
1

While I have posted this question quite a while ago, it is still quite active. Until now my solution was to stick with Doctrine 2.9 and keep using the merge function. Now I am working on new project which should be Doctrine 3 ready and should thus not use the merge anymore.

My solution is of course specific for my special use case. However, maybe it is also useful for other:

My Solution:

As described in the question I use the merge method to sync deserialized, external entities into the web database where a version of this entity might already exist (UPDATE required) or not (INSERT required).

@Merge Annotation

In my case entities have different properties where some might be relevant for syncing and must be merged while others are only used for (web) internal housekeeping and must not be merged. To tell these properties appart, I have created a custom @Merge annotation:

use Doctrine\Common\Annotations\Annotation;
    
/**
 * @Annotation
 * @Target("PROPERTY")
 */
final class SyncMerge { }

This annotation is then be used to mark the entities properties which should be merged:

class ToDoEntry {
    /*
     * @Merge
     */
    protected $date; 


    /*
     * @Merge
     */
    protected $title;

    // only used internally, no need to merge
    protected $someInternalValue;  

    ... 
}

Sync + Merge

During the sync process the annotation is used to merge the marked properties into existing entities:

public function mergeDeserialisedEntites(array $deserializedEntities, string $entityClass): void {
    foreach ($deserializedEntities as $deserializedEntity) {
        $classMergingInfos = $this->getMergingInfos($class);   
        $existingEntity = $this->entityManager->find($class, $deserializedEntity->getId());
        
        if (null !== $existingEntity) {
            // UPDATE existing entity
            // ==> Apply all properties marked by the Merge annotation
            foreach ($classMergingInfos as $propertyName => $reflectionProperty) {
                $deserializedValue = $reflectionProperty->getValue($deserializedEntity);
                $reflectionProperty->setValue($existingEntity, $deserializedEntity);
            }
            
            // Continue with existing entity to trigger update instead of insert on persist
            $deserializedEntity = $existingEntity;
        }

        // If $existingEntity was used an UPDATE will be triggerd
        // or an INSERT instead
        $this->entityManager->persist($deserializedEntity);
    }

    $this->entityManager->flush();
}

private $mergingInfos = [];
private function getMergingInfos($class) {
    if (!isset($this->mergingInfos[$class])) {
        $reflectionClass = new \ReflectionClass($class);
        $classProperties = $reflectionClass->getProperties();
        
        $propertyInfos = [];
        
        // Check which properties are marked by @Merge annotation and save information
        foreach ($classProperties as $reflectionProperty) {
            $annotation = $this->annotationReader->getPropertyAnnotation($reflectionProperty, Merge::class);
            
            if ($annotation instanceof Merge) { 
                $reflectionProperty->setAccessible(true);
                $propertyInfos[$reflectionProperty->getName()] = $reflectionProperty;
            }
        }
        
        $this->mergingInfos[$class] = $propertyInfos;
    }
    
    return $this->mergingInfos[$class];
}

That's it. If new properties are added to an entity I have only to decide whether it should be merged or not and add the annotation if needed. No need to update the sync code.

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225
0

Actually the code to handle this can be just a few lines. In background Doctrine will issue a query to search for your entity if not already in memory, so you can do the same by doing the query yourself with result cache enabled, and then just use PropertyAccessor to map the data.

https://symfony.com/doc/current/components/property_access.html

See this gist for a POC https://gist.github.com/stevro/99060106bbe54d64d3fbcf9a61e6a273

Stev
  • 1,062
  • 11
  • 23