Search code examples
symfonydoctrine-orm

How to replace EntityManager::merge in Doctrine 3?


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 whether an insert or update is required.

How to 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?


Solution

  • 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.