Có cách nào tích hợp sẵn để lấy tất cả các trường đã thay đổi / cập nhật trong thực thể Doctrine 2 không


81

Giả sử tôi truy xuất một thực thể $evà sửa đổi trạng thái của nó bằng bộ định tuyến:

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

Có khả năng nào để lấy một mảng các trường đã được thay đổi không?

Trong trường hợp ví dụ của tôi, tôi muốn lấy foo => a, bar => bkết quả là

Tái bút: vâng, tôi biết tôi có thể sửa đổi tất cả các trình truy cập và triển khai tính năng này theo cách thủ công, nhưng tôi đang tìm một số cách tiện dụng để thực hiện việc này

Câu trả lời:


148

Bạn có thể sử dụng Doctrine\ORM\EntityManager#getUnitOfWorkđể có được một Doctrine\ORM\UnitOfWork.

Sau đó, chỉ cần kích hoạt tính toán tập thay đổi (chỉ hoạt động trên các thực thể được quản lý) thông qua Doctrine\ORM\UnitOfWork#computeChangeSets().

Bạn cũng có thể sử dụng các phương pháp tương tự như Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)nếu bạn biết chính xác những gì bạn muốn kiểm tra mà không cần lặp lại trên toàn bộ biểu đồ đối tượng.

Sau đó, bạn có thể sử dụng Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)để truy xuất tất cả các thay đổi đối với đối tượng của mình.

Kết hợp nó lại với nhau:

$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);

Ghi chú. Nếu cố gắng lấy các trường cập nhật bên trong trình nghe preUpdate , đừng tính toán lại tập hợp thay đổi, vì nó đã được thực hiện. Chỉ cần gọi getEntityChangeSet để nhận tất cả các thay đổi được thực hiện đối với thực thể.

Cảnh báo: Như đã giải thích trong phần nhận xét, giải pháp này không nên được sử dụng bên ngoài trình nghe sự kiện Doctrine. Điều này sẽ phá vỡ hành vi của Doctrine.


4
Nhận xét bên dưới nói rằng nếu bạn gọi $ em-> computerChangeSets (), nó sẽ phá vỡ $ em-> Kiên trì () thông thường mà bạn gọi sau đó vì nó sẽ không có gì thay đổi cả. Nếu vậy, giải pháp là gì, chúng ta chỉ không gọi hàm đó?
Chadwick Meyer

4
Bạn không được phép sử dụng API này bên ngoài trình xử lý sự kiện vòng đời của UnitOfWork.
Ocramius

6
Bạn không nên. Đó không phải là những gì ORM được sử dụng để làm. Sử dụng thủ công khác nhau trong những trường hợp như vậy, bằng cách giữ một bản sao dữ liệu trước và sau các thao tác được áp dụng.
Ocramius

6
@Ocramius, nó có thể không phải là những gì nó được sử dụng để làm gì, nhưng nó chắc chắn sẽ hữu ích . Giá như có cách sử dụng Doctrine để tính toán những thay đổi mà không có tác dụng phụ. Ví dụ: nếu có một phương thức / lớp mới, có lẽ trong UOW, mà bạn có thể gọi để yêu cầu một loạt các thay đổi. Nhưng điều này sẽ không thay đổi / ảnh hưởng đến chu kỳ tồn tại thực tế theo bất kỳ cách nào. Điều đó có thể không?
caponica

3
Xem giải pháp tốt hơn gửi bởi Mohamed Ramrami dưới đây sử dụng $ em-> getUnitOfWork () -> getOriginalEntityData ($ tổ chức)
Wax Cage

41

Dấu hiệu cẩn thận cho những ai muốn kiểm tra các thay đổi trên thực thể bằng cách sử dụng phương pháp được mô tả ở trên.

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

Các $uow->computeChangeSets()phương pháp được sử dụng trong nội bộ bởi những thói quen kiên trì trong một cách mà làm cho giải pháp không sử dụng được ở trên. Đó cũng là những gì được viết trong các ý kiến để phương pháp: @internal Don't call from the outside. Sau khi kiểm tra các thay đổi đối với các thực thể $uow->computeChangeSets(), đoạn mã sau được thực thi ở cuối phương pháp (cho mỗi thực thể được quản lý):

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

Các $actualDatamảng chứa những thay đổi hiện tại để thuộc tính của thực thể. Ngay sau khi chúng được viết vào $this->originalEntityData[$oid], những thay đổi chưa tồn tại này được coi là thuộc tính ban đầu của thực thể.

Sau đó, khi $em->persist($entity)được gọi để lưu các thay đổi đối với thực thể, nó cũng liên quan đến phương thức $uow->computeChangeSets(), nhưng bây giờ nó sẽ không thể tìm thấy các thay đổi đối với thực thể, vì những thay đổi chưa tồn tại này được coi là thuộc tính ban đầu của thực thể .


1
Nó giống hệt như những gì @Ocramius đã chỉ định trong câu trả lời đã chọn
zerkms

1
$ uow = clone $ em-> getUnitOfWork (); phá được rằng vấn đề
tvlooy

1
Sao chép UoW không được hỗ trợ và có thể dẫn đến kết quả không mong muốn.
Ocramius

9
@Slavik Derevianko vậy bạn đề xuất gì? Chỉ cần không gọi $uow->computerChangeSets()? hoặc phương pháp thay thế nào?
Chadwick Meyer

Mặc dù bài đăng này thực sự hữu ích (nó là một cảnh báo lớn cho câu trả lời ở trên) nhưng bản thân nó không phải là giải pháp. Thay vào đó, tôi đã chỉnh sửa câu trả lời được chấp nhận.
Matthieu Napoli

37

Kiểm tra chức năng công khai (và không nội bộ) này:

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

Từ repo học thuyết :

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

Tất cả những gì bạn phải làm là triển khai một toArrayhoặc serializechức năng trong thực thể của mình và tạo sự khác biệt. Một cái gì đó như thế này:

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

1
Làm thế nào để áp dụng điều này cho tình huống khi Đối tượng có liên quan đến một Đối tượng khác (có thể là OneToOne)? Trường hợp này khi tôi chạy getOriginalEntityData trên Top-lvl Entity, dữ liệu gốc của các đối tượng liên quan của nó không thực sự là nguyên bản mà là được cập nhật.
mu4ddi3

5

Bạn có thể theo dõi các thay đổi bằng chính sách Thông báo .

Đầu tiên, triển khai giao diện NotifyPropertyChanged :

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

    private $_listeners = array();

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

Sau đó, chỉ cần gọi _onPropertyChanged trên mọi phương thức thay đổi dữ liệu ném thực thể của bạn như bên dưới:

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
Người nghe bên trong một thực thể ?! Điên cuồng! Nghiêm túc mà nói, chính sách theo dõi có vẻ là một giải pháp tốt, có cách nào để xác định người nghe bên ngoài thực thể không (Tôi đang sử dụng Symfony2 DoctrineBundle).
Gildas

Đây là giải pháp sai lầm. Bạn nên xem xét các sự kiện miền. github.com/gpslab/domain-event
ghost404 Ngày

3

Nó sẽ trả về các thay đổi

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

nó quá rõ ràng.
Rawburner

2

Trong trường hợp ai đó vẫn quan tâm đến một cách khác với câu trả lời được chấp nhận (nó không hoạt động với tôi và tôi thấy nó lộn xộn hơn cách này theo ý kiến ​​cá nhân của tôi).

Tôi đã cài đặt JMS Serializer Bundle và trên từng thực thể và trên từng thuộc tính mà tôi coi là thay đổi, tôi đã thêm @Group ({"change_entity_group"}). Bằng cách này, sau đó tôi có thể tạo tuần tự hóa giữa thực thể cũ và thực thể được cập nhật và sau đó, chỉ cần nói $ oldJson == $ updatedJson. Nếu các thuộc tính mà bạn quan tâm hoặc bạn muốn xem xét thay đổi thì JSON sẽ không giống nhau và nếu bạn thậm chí muốn đăng ký GÌ đã thay đổi cụ thể thì bạn có thể biến nó thành một mảng và tìm kiếm sự khác biệt.

Tôi đã sử dụng phương pháp này vì tôi chủ yếu quan tâm đến một vài thuộc tính của một loạt các thực thể chứ không phải trong thực thể hoàn toàn. Một ví dụ mà điều này sẽ hữu ích là nếu bạn có @PrePersist @PreUpdate và bạn có ngày last_update, ngày đó sẽ luôn được cập nhật, do đó bạn sẽ luôn nhận được rằng thực thể đã được cập nhật bằng cách sử dụng đơn vị công việc và những thứ tương tự.

Hy vọng phương pháp này hữu ích cho bất cứ ai.


1

Vậy ... phải làm gì khi chúng ta muốn tìm một tập hợp thay đổi bên ngoài vòng đời của Học thuyết? Như đã đề cập trong nhận xét của tôi về bài đăng của @Ocramius ở trên, có lẽ có thể tạo một phương thức "chỉ đọc" không gây rối với sự bền bỉ của Doctrine thực tế nhưng cung cấp cho người dùng cái nhìn về những gì đã thay đổi.

Đây là một ví dụ về những gì tôi đang nghĩ đến ...

/**
 * 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];
}

Nó được viết ở đây như một phương thức tĩnh nhưng có thể trở thành một phương thức bên trong UnitOfWork ...?

Tôi không bắt kịp tất cả các nội dung bên trong của Doctrine, vì vậy có thể đã bỏ sót điều gì đó có tác dụng phụ hoặc hiểu sai một phần về những gì phương pháp này thực hiện, nhưng một thử nghiệm (rất) nhanh về nó dường như cho tôi kết quả mà tôi mong đợi nhìn.

Tôi hi vọng nó sẽ giúp ích cho mọi người!


1
Chà, nếu chúng ta gặp nhau, bạn sẽ nhận được điểm cao năm rõ nét! Cảm ơn bạn rất, rất nhiều vì điều này. Rất dễ sử dụng trong 2 chức năng khác: hasChangesgetChanges( chức năng thứ hai để chỉ lấy các trường đã thay đổi thay vì toàn bộ tập hợp thay đổi).
rkeet

0

Trong trường hợp của tôi, để đồng bộ hóa dữ liệu từ điều khiển từ xa WSđến cục bộ, DBtôi đã sử dụng cách này để so sánh hai thực thể (kiểm tra xem thực thể cũ có khác với thực thể đã chỉnh sửa không).

Tôi thực sự nhân bản thực thể tồn tại để có hai đối tượng không tồn tại:

<?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);

Một khả năng khác thay vì so sánh các đối tượng trực tiếp:

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

trong trường hợp của tôi, tôi muốn nhận giá trị cũ của quan hệ trong thực thể, vì vậy tôi sử dụng Doctrine \ ORM \ PersentlyCollection :: getSnapshot dựa trên cơ sở này


0

Nó hoạt động đối với tôi 1. import EntityManager 2. Bây giờ bạn có thể sử dụng nó ở bất kỳ đâu trong lớp.

  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

Làm việc với UnitOfWorkcomputeChangeSets bên trong một Người nghe Sự kiện của Học thuyết có lẽ là phương pháp được ưa thích.

Tuy nhiên : Nếu bạn muốn tồn tại và tạo ra một thực thể mới trong trình lắng nghe này, bạn có thể gặp phải rất nhiều rắc rối. Có vẻ như, người nghe thích hợp duy nhất sẽ onFlushtự giải quyết vấn đề của chính nó.

Vì vậy, tôi đề xuất một so sánh đơn giản nhưng nhẹ, có thể được sử dụng trong Bộ điều khiển và thậm chí cả Dịch vụ bằng cách chỉ cần tiêm EntityManagerInterface(lấy cảm hứng từ @Mohamed Ramrami trong bài đăng ở trên):

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

Lưu ý : nó thực hiện so sánh các chuỗi, ngày giờ, bools, số nguyên và số nổi một cách chính xác nhưng không thành công trên các đối tượng (do các vấn đề tham chiếu Thông tư). Người ta có thể so sánh các đối tượng này sâu hơn, nhưng ví dụ như phát hiện thay đổi văn bản, điều này là đủ và đơn giản hơn nhiều so với việc xử lý Trình xử lý sự kiện.

Thêm thông tin:

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.