Doctrine 2 엔터티에서 변경 / 업데이트 된 모든 필드를 가져 오는 기본 제공 방법이 있습니까?


81

엔티티를 검색하고 $esetter로 상태를 수정 한다고 가정 해 보겠습니다 .

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

변경된 필드 배열을 검색 할 가능성이 있습니까?

내 예의 경우 foo => a, bar => b결과 로 검색하고 싶습니다.

추신 : 예, 모든 접근자를 수정하고이 기능을 수동으로 구현할 수 있다는 것을 알고 있지만이 작업을 수행 할 수있는 편리한 방법을 찾고 있습니다.

답변:


148

을 사용 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 listener 내 에서 업데이트 된 필드를 가져 오려는 경우 이미 완료되었으므로 변경 세트를 다시 계산하지 마십시오. getEntityChangeSet를 호출하여 엔티티에 대한 모든 변경 사항을 가져 오십시오.

경고 : 주석에서 설명했듯이이 솔루션은 Doctrine 이벤트 리스너 외부에서 사용해서는 안됩니다. 이것은 교리의 행동을 깨뜨릴 것입니다.


4
아래 주석은 $ em-> computerChangeSets ()를 호출하면 아무것도 변경되지 않은 것처럼 보이기 때문에 나중에 호출하는 일반 $ em-> persist ()를 중단한다고 말합니다. 그렇다면 해결책은 무엇입니까? 그 함수를 호출하지 않습니까?
Chadwick Meyer

4
UnitOfWork의 수명주기 이벤트 리스너 외부에서이 API를 사용해서는 안됩니다.
Ocramius

6
해서는 안됩니다. 그것은 ORM이 사용되는 목적이 아닙니다. 이러한 경우 적용된 작업 전후에 데이터 사본을 보관하여 수동 비교를 사용하십시오.
Ocramius

6
@Ocramius, 사용 목적이 아닐 수도 있지만 의심 할 여지없이 유용 할 것 입니다. 부작용없이 변화를 계산하기 위해 교리를 사용할 수있는 방법이 있다면. 예를 들어 UOW에 새 메서드 / 클래스가있는 경우 변경 사항 배열을 요청하기 위해 호출 할 수 있습니다. 그러나 이는 어떤 식 으로든 실제 지속성주기를 변경 / 영향을 미치지 않습니다. 가능합니까?
caponica 2014-08-22

3
> getOriginalEntityData ($ 엔티티) - () $ 보하기> getUnitOfWork를 사용하여 노호 모하메드 Ramrami에 의해 게시 더 나은 솔루션을 참조하십시오
왁스 케이지

41

위에서 설명한 방법을 사용하여 엔티티의 변경 사항을 확인하려는 사람들을 위해 큰 조심하십시오 .

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

$uow->computeChangeSets()메서드는 위의 솔루션을 사용할 수 없게 만드는 방식으로 지속 루틴에 의해 내부적으로 사용됩니다. 그것은 또한 메소드에 대한 주석에 쓰여진 것입니다 : @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()위해이 호출 될 때 메서드도 포함 되지만 아직 유지되지 않은 변경 사항은 엔터티의 원래 속성으로 간주되므로 엔터티의 변경 사항을 찾을 수 없습니다. .


1
확인 된 답변에서 @Ocramius가 지정한 것과 똑같습니다
zerkms

1
$ uow = clone $ em-> getUnitOfWork (); 해결할 수있는 문제 그 문제
tvlooy

1
UoW 복제는 지원되지 않으며 원하지 않는 결과가 발생할 수 있습니다.
Ocramius 2014

9
@Slavik Derevianko 그래서 당신은 무엇을 제안합니까? 전화 하지마 $uow->computerChangeSets()? 또는 어떤 대체 방법?
Chadwick Meyer

이 게시물은 정말 유용하지만 (위의 답변에 대한 큰 경고입니다) 그 자체로는 해결책이 아닙니다. 대신 수락 된 답변을 편집했습니다.
Matthieu Napoli

37

이 공용 (내부 아님) 기능을 확인하십시오.

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

교리 저장소에서 :

/**
 * 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)

당신이해야 할 일은 당신의 엔티티에 toArrayor serialize함수를 구현 하고 비교하는 것입니다. 이 같은 :

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

1
엔터티가 다른 엔터티 (OneToOne 일 수 있음)와 관련된 상황에이를 적용하는 방법은 무엇입니까? 이 경우 top-lvl Entity에서 getOriginalEntityData를 실행하면 관련 엔티티 원본 데이터가 실제로 원본이 아니라 업데이트됩니다.
mu4ddi3

5

알림 정책을 사용 하여 변경 사항을 추적 할 수 있습니다 .

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

7
엔티티 내부의 청취자?! 광기! 진지하게, 추적 정책은 좋은 해결책처럼 보입니다. 엔티티 외부에서 리스너를 정의 할 수있는 방법이 있습니까 (저는 Symfony2 DoctrineBundle을 사용하고 있습니다).
Gildas 2014

이것은 잘못된 해결책입니다. 도메인 이벤트를 살펴 봐야합니다. github.com/gpslab/domain-event
ghost404


2

누군가가 받아 들인 대답과 다른 방식에 여전히 관심이있는 경우 (나에게 효과가 없었고 개인적인 의견으로는이 방식보다 더 지저분하다는 것을 알았습니다).

JMS Serializer Bundle을 설치하고 각 엔티티와 변경을 고려하는 각 속성에 @Group ({ "changed_entity_group"})을 추가했습니다. 이렇게하면 이전 엔터티와 업데이트 된 엔터티간에 직렬화를 수행 할 수 있습니다. 그 후에는 $ oldJson == $ updatedJson이라고 말하면됩니다. 관심이 있거나 고려하고 싶은 속성이 변경되면 JSON이 동일하지 않으며 특정 변경 사항을 등록하려는 경우 배열로 변환하고 차이점을 검색 할 수 있습니다.

나는 주로 엔티티 전체가 아니라 여러 엔티티의 몇 가지 속성에 관심이 있었기 때문에이 방법을 사용했습니다. 이것이 유용한 예는 @PrePersist @PreUpdate가 있고 last_update 날짜가있는 경우 항상 업데이트되므로 항상 작업 단위 및 이와 같은 항목을 사용하여 엔티티가 업데이트되었음을 ​​알 수 있습니다.

이 방법이 누구에게나 도움이되기를 바랍니다.


1

그래서 ... 우리가 교리 라이프 사이클 밖에서 변경 세트를 찾고 싶을 때 어떻게해야할까요? 위의 @Ocramius '게시물에 대한 내 의견에서 언급했듯이 실제 교리 지속성을 엉망으로 만들지 않고 사용자에게 변경된 내용을 볼 수있는 "읽기 전용"메서드를 만드는 것이 가능할 것입니다.

여기 제가 생각하는 것의 예가 있습니다 ...

/**
 * 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
글쎄, 우리가 만난다면, 당신은 선명한 하이 파이브를 얻습니다! 이것에 대해 대단히 감사합니다. 두 가지 다른 기능에서도 사용하기 매우 쉽습니다. hasChanges그리고 getChanges(후자는 전체 변경 세트 대신 변경된 필드 만 가져옵니다).
rkeet

0

제 경우에는 원격 WS에서 로컬로 데이터를 동기화 DB하기 위해이 방법을 사용하여 두 엔티티를 비교했습니다 (오래된 엔티티가 편집 된 엔티티와 다른지 확인하십시오).

지속되지 않는 두 개체를 갖도록 지속 된 엔티티를 Symply 복제합니다.

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

0

제 경우에는 엔티티에서 관계의 이전 값을 얻고 싶으므로 Doctrine \ ORM \ PersistentCollection :: getSnapshot 기반을 사용 합니다.


0

그것은 나를 위해 작동합니다 1. import 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';
    }

0

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

참고 : 문자열, 날짜 시간, 부울, 정수 및 부동 소수점을 올바르게 비교하지만 객체에서는 실패합니다 (순환 참조 문제로 인해). 이러한 객체를 더 심도있게 비교할 수 있지만 예를 들어 텍스트 변경 감지의 경우 이벤트 리스너를 처리하는 것보다 충분하고 훨씬 간단합니다.

더 많은 정보:

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.