【问题标题】:Is there a built-in way to get all of the changed/updated fields in a Doctrine 2 entity是否有内置方法来获取 Doctrine 2 实体中所有更改/更新的字段
【发布时间】:2012-02-21 20:59:14
【问题描述】:

假设我检索了一个实体 $e 并使用 setter 修改它的状态:

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

是否有可能检索已更改的字段数组?

在我的示例中,我想检索 foo => a, bar => b 作为结果

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

【问题讨论】:

    标签: php symfony doctrine-orm doctrine


    【解决方案1】:

    你可以使用 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 即可获取对实体所做的所有更改。

    警告: 如 cmets 中所述,此解决方案不应在 Doctrine 事件侦听器之外使用。这将破坏 Doctrine 的行为。

    【讨论】:

    • 下面的评论说如果你调用 $em->computerChangeSets() 它将破坏你稍后调用的常规 $em->persist() 因为它看起来没有任何改变.如果是这样,解决方案是什么,我们不调用那个函数吗?
    • 您不应该在 UnitOfWork 的生命周期事件侦听器之外使用此 API。
    • 你不应该。这不是 ORM 的用途。在这种情况下使用手动差异,通过在应用操作之前和之后保留数据的副本。
    • @Ocramius,它可能不是它的本意,但它无疑是有用的。如果有一种方法可以使用 Doctrine 来计算没有副作用的变化。例如。如果有一个新的方法/类,可能在 UOW 中,您可以调用它来请求一系列更改。但这不会以任何方式改变/影响实际的持久性周期。这可能吗?
    • 查看 Mohamed Ramrami 使用 $em->getUnitOfWork()->getOriginalEntityData($entity) 发布的更好的解决方案
    【解决方案2】:

    检查此公共(而非内部)功能:

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

    来自教义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);
    

    【讨论】:

    • 如何将其应用于实体与另一个实体相关的情况(可以是 OneToOne)?这种情况下,当我在*实体上运行 getOriginalEntityData 时,其相关实体的原始数据并不是真正的原始数据,而是更新了。
    【解决方案3】:

    小心谨慎的标志用于那些想要使用上述方法检查实体更改的人。

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

    $uow->computeChangeSets() 方法由持久化例程在内部使用,导致上述解决方案无法使用。这也是方法的 cmets 中写入的内容:@internal Don't call from the outside。 在使用$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(),但是现在找不到实体的更改,因为这些还没有持久化更改被视为实体的原始属性。

    【讨论】:

    • 这与@Ocramius 在检查答案中指定的完全相同
    • $uow = 克隆 $em->getUnitOfWork();解决了这个问题
    • 不支持克隆 UoW,可能会导致不良结果。
    • @Slavik Derevianko 那么你有什么建议?只是不要打电话给$uow->computerChangeSets()?或者有什么替代方法?
    • 虽然这篇文章真的很有用(这是对上述答案的一个重大警告),但它本身并不是一个解决方案。我已经编辑了接受的答案。
    【解决方案4】:

    您可以使用Notify policies 跟踪更改。

    首先,实现NotifyPropertyChanged接口:

    /**
     * @Entity
     * @ChangeTrackingPolicy("NOTIFY")
     */
    class MyEntity implements NotifyPropertyChanged
    {
        // ...
    
        private $_listeners = array();
    
        public function addPropertyChangedListener(PropertyChangedListener $listener)
        {
            $this->_listeners[] = $listener;
        }
    }
    

    然后,只需在更改数据的每个方法上调用 _onPropertyChanged 就会抛出您的实体,如下所示:

    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;
            }
        }
    }
    

    【讨论】:

    • 实体内的监听器?!疯狂!说真的,跟踪策略看起来是一个很好的解决方案,有没有办法在实体之外定义监听器(我使用的是 Symfony2 DoctrineBundle)。
    • 这是错误的解决方案。您应该查看域事件。 github.com/gpslab/domain-event
    【解决方案5】:

    它将返回更改

    $entityManager->getUnitOfWork()->getEntityChangeSet($entity)
    

    【讨论】:

    • 太明显了。
    【解决方案6】:

    那么...当我们想要在 Doctrine 生命周期之外找到变更集时该怎么办?正如我在上面对@Ocramius 帖子的评论中提到的那样,也许可以创建一个“只读”方法,它不会与实际的 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 的所有内部细节,所以可能错过了一些有副作用的东西或误解了该方法的部分功能,但对它的(非常)快速测试似乎给了我我希望看到的结果。

    我希望这对某人有帮助!

    【讨论】:

    • 好吧,如果我们见面,你会得到一个清脆的高五!非常非常感谢这个。也很容易在其他 2 个函数中使用:hasChangesgetChanges(后者仅获取更改的字段而不是整个变更集)。
    【解决方案7】:

    如果有人仍然对与接受的答案不同的方式感兴趣(它对我不起作用,我个人认为它比这种方式更混乱)。

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

    我之所以使用这种方法,是因为我主要对一堆实体的一些属性感兴趣,而不是对整个实体感兴趣。这很有用的一个示例是,如果您有一个 @PrePersist @PreUpdate 并且您有一个 last_update 日期,该日期将始终更新,因此您将始终使用工作单元和类似的东西更新实体。

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

    【讨论】:

      【解决方案8】:

      在我的情况下,我想获得实体中关系的旧值,所以我使用 Doctrine\ORM\PersistentCollection::getSnapshot 基于this

      【讨论】:

        【解决方案9】:

        在我的例子中,对于从远程 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
        }
        

        【讨论】:

          【解决方案10】:

          它对我有用 1.导入EntityManager 2. 现在你可以在课堂的任何地方使用它了。

            use Doctrine\ORM\EntityManager;
          
          
          
              $preData = $this->em->getUnitOfWork()->getOriginalEntityData($entity);
              // $preData['active'] for old data and $entity->getActive() for new data
              if($preData['active'] != $entity->getActive()){
                  echo 'Send email';
              }
          

          【讨论】:

            【解决方案11】:

            使用UnitOfWorkcomputeChangeSets在 Doctrine 事件监听器中可能是首选方法。

            然而:如果你想在这个监听器中持久化和刷新一个新实体,你可能会遇到很多麻烦。看起来,唯一合适的听众是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;
                    }
                }
            }
            

            注意:它会正确比较字符串、日期时间、布尔值、整数和浮点数,但是在对象上会失败(由于循环引用问题)。人们可以更深入地比较这些对象,但例如文本更改检测这已经足够了,而且比处理事件监听器要简单得多。

            更多信息:

            【讨论】: