有没有一种内置的方法可以获取Doctrine 2实体中所有更改/更新的字段?

92

假设我检索到一个实体$e并使用setter方法修改其状态:

$e->setFoo('a');
$e->setBar('b');

有没有可能检索出已更改的字段数组?

就我的例子而言,我希望能够检索出foo => a, bar => b作为结果。

PS:是的,我知道我可以修改所有访问器并手动实现此功能,但我正在寻找一些方便的方法来完成此操作。

11个回答

163
你可以使用Doctrine\ORM\EntityManager#getUnitOfWork获取一个Doctrine\ORM\UnitOfWork实例。
然后,通过Doctrine\ORM\UnitOfWork#computeChangeSets()触发变更集合计算(仅适用于已管理的实体)。
如果你知道需要检查什么而不必遍历整个对象图,则还可以使用类似的方法,例如Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)
完成上述步骤后,您可以使用Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)来检索对象的所有更改内容。
将它们组合在一起:
$entity = $em->find('My\Entity', 1);
$entity->setTitle('Changed Title!');
$uow = $em->getUnitOfWork();
$uow->computeChangeSets(); // do not compute changes if inside a listener
$changeset = $uow->getEntityChangeSet($entity);

注意:在尝试从preUpdate监听器中获取更新字段时,不要重新计算更改集,因为它已经完成。只需调用getEntityChangeSet获取对实体所做的所有更改。

警告:如评论中所述,此解决方案不应在Doctrine事件监听器之外使用,否则会破坏Doctrine的行为。


5
下面的评论说,如果调用$em->computerChangeSets()函数,则会破坏后面调用的常规$em->persist()函数,因为它看起来没有任何变化。如果确实如此,解决方案是什么?我们只需不调用那个函数吗? - Chadwick Meyer
5
不建议在UnitOfWork的生命周期事件监听器外使用此API。 - Ocramius
8
不应该这样做。ORM不是用来处理这种情况的。在这种情况下,请使用手动比较,通过在应用操作之前和之后保留数据副本来进行比较。 - Ocramius
7
@Ocramius,这可能不是它的预定用途,但毫无疑问地会非常“有用”。如果只有一种方法可以使用Doctrine来计算更改而不产生任何副作用就好了。例如,如果在UOW中有一个新的方法/类,你可以调用它来请求更改数组,但不会以任何方式修改/影响实际的持久性循环。那是否有可能呢? - caponica
4
请看下面Mohamed Ramrami发布的更好解决方案,使用$ em-> getUnitOfWork()-> getOriginalEntityData($ entity)函数。 - Wax Cage
显示剩余6条评论

49

请检查这个公共(非内部)函数:

$this->em->getUnitOfWork()->getOriginalEntityData($entity);

来自Doctrine repo

/**
 * Gets the original data of an entity. The original data is the data that was
 * present at the time the entity was reconstituted from the database.
 *
 * @param object $entity
 *
 * @return array
 */
public function getOriginalEntityData($entity)

您只需要在实体中实现 toArrayserialize 函数并进行比较即可。就像这样:

$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);

7
如何将这个方法应用到实体与另一个实体相关联的情况(可以是一对一的关系)?当我在顶层实体上运行getOriginalEntityData时,它相关的实体原始数据并不真正是原始的,而是已更新过的。 - mu4ddi3

44

强烈警示:那些想要使用上述方法检查实体变化的人需要小心。

$uow = $em->getUnitOfWork();
$uow->computeChangeSets();

$uow->computeChangeSets()方法在持久化过程中以一种使上述解决方案无法使用的方式内部使用。这也是该方法注释中所写的:@internal 不要从外部调用。 在使用$uow->computeChangeSets()检查实体更改后,该方法的末尾将执行以下代码(针对每个受管理的实体):

if ($changeSet) {
    $this->entityChangeSets[$oid]   = $changeSet;
    $this->originalEntityData[$oid] = $actualData;
    $this->entityUpdates[$oid]      = $entity;
}

$actualData数组保存实体属性的当前更改。一旦这些更改被写入$this->originalEntityData[$oid],那么这些尚未持久化的更改就被视为实体的原始属性。

稍后,当调用$em->persist($entity)来保存实体的更改时,它也涉及到方法$uow->computeChangeSets(),但此时它将无法找到实体的更改,因为这些尚未持久化的更改被视为实体的原始属性。


1
这正是@Ocramius在选定的答案中指定的内容。 - zerkms
1
$uow = clone $em->getUnitOfWork(); 解决了那个问题。 - tvlooy
1
不支持克隆UoW,可能会导致不良后果。 - Ocramius
9
那你建议怎么做?是不是不调用 $uow->computerChangeSets() 方法?还是有其他替代方法? - Chadwick Meyer
虽然这篇文章非常有用(它是对上面答案的一个重要警告),但它本身并不是一个解决方案。我已经编辑了被接受的答案。 - Matthieu Napoli

7
它将返回更改。
$entityManager->getUnitOfWork()->getEntityChangeSet($entity)

这是非常显而易见的。 - Rawburner
在进行任何更改之前,请不要忘记执行 $uow->computeChangeSets();。 - undefined

5
你可以通过通知策略跟踪变化。
首先,实现NotifyPropertyChanged接口:

/**
 * @Entity
 * @ChangeTrackingPolicy("NOTIFY")
 */
class MyEntity implements NotifyPropertyChanged
{
    // ...

    private $_listeners = array();

    public function addPropertyChangedListener(PropertyChangedListener $listener)
    {
        $this->_listeners[] = $listener;
    }
}

Then, just call the _onPropertyChanged on every method that changes data throw your entity as below:

class MyEntity implements NotifyPropertyChanged
{
    // ...

    protected function _onPropertyChanged($propName, $oldValue, $newValue)
    {
        if ($this->_listeners) {
            foreach ($this->_listeners as $listener) {
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
            }
        }
    }

    public function setData($data)
    {
        if ($data != $this->data) {
            $this->_onPropertyChanged('data', $this->data, $data);
            $this->data = $data;
        }
    }
}


8
实体内部有监听器?!疯了!说真的,跟踪策略看起来是一个好的解决方案,有没有办法在实体外定义监听器(我正在使用Symfony2 DoctrineBundle)? - Gildas
这不是正确的解决方案。你应该看向领域事件。https://github.com/gpslab/domain-event - ghost404

3

那么,在Doctrine生命周期外想要找到更改集怎么办呢?如我在@Ocramius的帖子评论中提到的那样,也许可以创建一个“readonly”方法,它不会干扰实际的Doctrine持久性,但给用户一种已更改的视图。

以下是我所思考的示例...

/**
 * Try to get an Entity changeSet without changing the UnitOfWork
 *
 * @param EntityManager $em
 * @param $entity
 * @return null|array
 */
public static function diffDoctrineObject(EntityManager $em, $entity) {
    $uow = $em->getUnitOfWork();

    /*****************************************/
    /* Equivalent of $uow->computeChangeSet($this->em->getClassMetadata(get_class($entity)), $entity);
    /*****************************************/
    $class = $em->getClassMetadata(get_class($entity));
    $oid = spl_object_hash($entity);
    $entityChangeSets = array();

    if ($uow->isReadOnly($entity)) {
        return null;
    }

    if ( ! $class->isInheritanceTypeNone()) {
        $class = $em->getClassMetadata(get_class($entity));
    }

    // These parts are not needed for the changeSet?
    // $invoke = $uow->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
    // 
    // if ($invoke !== ListenersInvoker::INVOKE_NONE) {
    //     $uow->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($em), $invoke);
    // }

    $actualData = array();

    foreach ($class->reflFields as $name => $refProp) {
        $value = $refProp->getValue($entity);

        if ($class->isCollectionValuedAssociation($name) && $value !== null) {
            if ($value instanceof PersistentCollection) {
                if ($value->getOwner() === $entity) {
                    continue;
                }

                $value = new ArrayCollection($value->getValues());
            }

            // If $value is not a Collection then use an ArrayCollection.
            if ( ! $value instanceof Collection) {
                $value = new ArrayCollection($value);
            }

            $assoc = $class->associationMappings[$name];

            // Inject PersistentCollection
            $value = new PersistentCollection(
                $em, $em->getClassMetadata($assoc['targetEntity']), $value
            );
            $value->setOwner($entity, $assoc);
            $value->setDirty( ! $value->isEmpty());

            $class->reflFields[$name]->setValue($entity, $value);

            $actualData[$name] = $value;

            continue;
        }

        if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
            $actualData[$name] = $value;
        }
    }

    $originalEntityData = $uow->getOriginalEntityData($entity);
    if (empty($originalEntityData)) {
        // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
        // These result in an INSERT.
        $originalEntityData = $actualData;
        $changeSet = array();

        foreach ($actualData as $propName => $actualValue) {
            if ( ! isset($class->associationMappings[$propName])) {
                $changeSet[$propName] = array(null, $actualValue);

                continue;
            }

            $assoc = $class->associationMappings[$propName];

            if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
                $changeSet[$propName] = array(null, $actualValue);
            }
        }

        $entityChangeSets[$oid] = $changeSet; // @todo - remove this?
    } else {
        // Entity is "fully" MANAGED: it was already fully persisted before
        // and we have a copy of the original data
        $originalData           = $originalEntityData;
        $isChangeTrackingNotify = $class->isChangeTrackingNotify();
        $changeSet              = $isChangeTrackingNotify ? $uow->getEntityChangeSet($entity) : array();

        foreach ($actualData as $propName => $actualValue) {
            // skip field, its a partially omitted one!
            if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
                continue;
            }

            $orgValue = $originalData[$propName];

            // skip if value haven't changed
            if ($orgValue === $actualValue) {
                continue;
            }

            // if regular field
            if ( ! isset($class->associationMappings[$propName])) {
                if ($isChangeTrackingNotify) {
                    continue;
                }

                $changeSet[$propName] = array($orgValue, $actualValue);

                continue;
            }

            $assoc = $class->associationMappings[$propName];

            // Persistent collection was exchanged with the "originally"
            // created one. This can only mean it was cloned and replaced
            // on another entity.
            if ($actualValue instanceof PersistentCollection) {
                $owner = $actualValue->getOwner();
                if ($owner === null) { // cloned
                    $actualValue->setOwner($entity, $assoc);
                } else if ($owner !== $entity) { // no clone, we have to fix
                    // @todo - what does this do... can it be removed?
                    if (!$actualValue->isInitialized()) {
                        $actualValue->initialize(); // we have to do this otherwise the cols share state
                    }
                    $newValue = clone $actualValue;
                    $newValue->setOwner($entity, $assoc);
                    $class->reflFields[$propName]->setValue($entity, $newValue);
                }
            }

            if ($orgValue instanceof PersistentCollection) {
                // A PersistentCollection was de-referenced, so delete it.
    // These parts are not needed for the changeSet?
    //            $coid = spl_object_hash($orgValue);
    //
    //            if (isset($uow->collectionDeletions[$coid])) {
    //                continue;
    //            }
    //
    //            $uow->collectionDeletions[$coid] = $orgValue;
                $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.

                continue;
            }

            if ($assoc['type'] & ClassMetadata::TO_ONE) {
                if ($assoc['isOwningSide']) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }

    // These parts are not needed for the changeSet?
    //            if ($orgValue !== null && $assoc['orphanRemoval']) {
    //                $uow->scheduleOrphanRemoval($orgValue);
    //            }
            }
        }

        if ($changeSet) {
            $entityChangeSets[$oid]     = $changeSet;
    // These parts are not needed for the changeSet?
    //        $originalEntityData         = $actualData;
    //        $uow->entityUpdates[$oid]   = $entity;
        }
    }

    // These parts are not needed for the changeSet?
    //// Look for changes in associations of the entity
    //foreach ($class->associationMappings as $field => $assoc) {
    //    if (($val = $class->reflFields[$field]->getValue($entity)) !== null) {
    //        $uow->computeAssociationChanges($assoc, $val);
    //        if (!isset($entityChangeSets[$oid]) &&
    //            $assoc['isOwningSide'] &&
    //            $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
    //            $val instanceof PersistentCollection &&
    //            $val->isDirty()) {
    //            $entityChangeSets[$oid]   = array();
    //            $originalEntityData = $actualData;
    //            $uow->entityUpdates[$oid]      = $entity;
    //        }
    //    }
    //}
    /*********************/

    return $entityChangeSets[$oid];
}

这里的措辞是静态方法,但可以成为UnitOfWork内部的一个方法……?

我不是很了解Doctrine的所有内部细节,所以可能错过了某些副作用或误解了该方法的某些部分,但对它进行(非常)快速测试似乎给出了我期望看到的结果。

希望能对某人有所帮助!


1
如果我们有机会见面,你将得到一个清脆的高五!非常感谢您提供的帮助。这个函数也很容易在另外两个函数中使用:hasChangesgetChanges(后者只获取更改的字段而不是整个更改集)。 - rkeet

2

如果有人对比起被接受的答案还有其他方法感兴趣(因为那个方法对我来说不起作用,而且在我个人看来比这个方法更混乱)。

我安装了JMS Serializer Bundle,然后在每个实体和我认为是更改的每个属性上添加了@Group({"changed_entity_group"})。这样,我就可以在旧实体和更新后的实体之间进行序列化,然后只需要说$oldJson == $updatedJson。如果您感兴趣或希望考虑更改的属性与您想要考虑更改的属性不同,则JSON将不同,甚至如果您想注册具体更改的内容,则可以将其转换为数组并搜索差异。

我使用这种方法,因为我主要关注一些实体的几个属性,而不是整个实体。一个例子是,如果您有一个@PrePersist @PreUpdate和您有一个last_update日期,那么它将始终更新,因此您将始终得到使用工作单元更新了该实体之类的东西。

希望这种方法对任何人都有帮助。


1

在我的情况下,我想要获取实体中关系的旧值,因此我使用Doctrine\ORM\PersistentCollection::getSnapshot基于this


1

在Doctrine事件监听器中,与UnitOfWorkcomputeChangeSets一起工作可能是首选方法。

然而:如果您想在此侦听器中持久化和刷新新实体,则可能会遇到很多麻烦。似乎唯一合适的侦听器是具有自己问题集的onFlush

因此,我建议进行简单但轻量级的比较,可以通过简单注入EntityManagerInterface在控制器甚至服务中使用(灵感来自上面的@Mohamed Ramrami):

$uow = $entityManager->getUnitOfWork();
$originalEntityData = $uow->getOriginalEntityData($blog);

// for nested entities, as suggested in the docs
$defaultContext = [
    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
        return $object->getId();
    },
];
$normalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer(null, null, null, null, null,  null, $defaultContext)]);
$yourEntityNormalized = $normalizer->normalize();
$originalNormalized = $normalizer->normalize($originalEntityData);

$changed = [];
foreach ($originalNormalized as $item=>$value) {
    if(array_key_exists($item, $yourEntityNormalized)) {
        if($value !== $yourEntityNormalized[$item]) {
            $changed[] = $item;
        }
    }
}

注意: 它可以正确比较字符串、日期时间、布尔值、整数和浮点数,但由于循环引用问题,在对象上失败。可以更深入地比较这些对象,但对于例如文本更改检测,这已足够简单,比处理事件监听器更简单。
更多信息:
- 序列化程序和规范化程序 - 处理循环引用

0
在我的情况下,为了将远程WS的数据同步到本地DB,我使用了这种方式来比较两个实体(检查旧实体与编辑后的实体是否有差异)。
我简单地克隆持久化实体,以获得两个未持久化的对象:
<?php

$entity = $repository->find($id);// original entity exists
if (null === $entity) {
    $entity    = new $className();// local entity not exists, create new one
}
$oldEntity = clone $entity;// make a detached "backup" of the entity before it's changed
// make some changes to the entity...
$entity->setX('Y');

// now compare entities properties/values
$entityCloned = clone $entity;// clone entity for detached (not persisted) entity comparaison
if ( ! $em->contains( $entity ) || $entityCloned != $oldEntity) {// do not compare strictly!
    $em->persist( $entity );
    $em->flush();
}

unset($entityCloned, $oldEntity, $entity);

除了直接比较对象之外,还有另一种可能性:

<?php
// here again we need to clone the entity ($entityCloned)
$entity_diff = array_keys(
    array_diff_key(
        get_object_vars( $entityCloned ),
        get_object_vars( $oldEntity )
    )
);
if(count($entity_diff) > 0){
    // persist & flush
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接