我正在开发一个基于
Symfony 2.8
的网络应用程序项目,当前使用 Doctrine 2
。该项目基本上是一个简单的待办事项列表应用程序,可以与移动应用程序(iOS/Android)同步。
在阅读
Doctrine 3
的更新说明时我发现,EntityManager::merge
将不再受支持。
ORM 3.0 没有提供 EntityManager#merge() 的替代方法, 因为合并语义应该是业务领域的一部分 而不是应用程序的持久域。如果你的 应用程序严重依赖于类似 CRUD 的交互和/或 PATCH 安静的操作,你应该考虑替代方案,例如 JMSSerializer。
我不确定什么是最好/正确的替代方法
EntityManager::merge
?
我在哪里使用合并:
在移动应用程序与 Web 应用程序同步期间,数据作为序列化 JSON 传输,然后由
JMSSerializer
反序列化到实体对象。当 Web 应用程序以这种方式收到 ToDoEntry
对象时,它可以是一个新的 ToDo-Entry(Web 应用程序中尚不知道)或更新的现有条目。无论哪种方式,接收到的对象都不受 EntityManager
管理。因此 $em->persist($receivedObject)
将始终尝试插入新对象。如果 Web 应用程序中已存在 ToDo-Entry 并且需要更新,则此操作将会失败(由于 id 的唯一约束)。
而是使用
$em->merge($receivedObject)
自动检查是否需要插入或更新。
如何解决这个问题?
当然,如果具有相同 ID 的实体已经存在,我可以检查每个收到的对象。在这种情况下,可以加载现有对象并手动更新其属性。然而,这会非常麻烦。当然,真正的项目使用许多不同的实体,每个实体类型/类都需要自己的处理来检查哪些属性需要更新。难道就没有更好的解决办法吗?
你可以尝试使用Doctrine\ORM\UnitOfWork的registerManaged()方法。
// $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);
当然,您可以在执行所需操作之前/之后使用 Doctrine\ORM\UnitOfWork 的 getEntityState() 检查实体的状态。
$this->eM->getUnitOfWork()->getEntityState($entity, $assert = 3)
$断言<-- 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.
虽然我很久以前就已经发布了这个问题,但它仍然很活跃。到目前为止,我的解决方案是坚持使用 Doctrine 2.9 并继续使用
merge
函数。现在我正在开发一个新项目,该项目应该已准备好 Doctrine 3,因此不应再使用 merge
。
我的解决方案当然是针对我的特殊用例的。然而,也许它对其他人也有用:
如问题中所述,我使用
merge
方法将反序列化的外部实体同步到 Web 数据库中,其中该实体的版本可能已存在(需要更新)或不存在(需要插入)。
在我的例子中,实体具有不同的属性,其中一些可能与同步相关并且必须合并,而其他属性仅用于(网络)内部管理并且不得合并。为了告诉这些属性,我创建了一个自定义
@Merge
注释:
use Doctrine\Common\Annotations\Annotation;
/**
* @Annotation
* @Target("PROPERTY")
*/
final class SyncMerge { }
此注释用于标记应合并的实体属性:
class ToDoEntry {
/*
* @Merge
*/
protected $date;
/*
* @Merge
*/
protected $title;
// only used internally, no need to merge
protected $someInternalValue;
...
}
在同步过程中,注释用于将标记的属性合并到现有实体中:
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];
}
就是这样。如果将新属性添加到实体中,我只需决定是否应该合并它,并在需要时添加注释。无需更新同步代码。
实际上处理这个问题的代码只需几行。在后台 Doctrine 将发出一个查询来搜索您的实体(如果尚未在内存中),因此您可以通过在启用结果缓存的情况下自行执行查询来执行相同的操作,然后只需使用 PropertyAccessor 来映射数据。
https://symfony.com/doc/current/components/property_access.html
请参阅此 POC 要点 https://gist.github.com/stevro/99060106bbe54d64d3fbcf9a61e6a273