Question

Consider the following schema:

[Work]
id
tags ManyToMany(targetEntity="Tag", inversedBy="works", cascade={"persist"})

[Tag]
id
works_count
works ManyToMany(targetEntity="Work", mappedBy="tags")

works_count is a counter cache for Tag::works.

I have a onFlush listener on Work that checks if Work::tags has changed, and updates each of the tags' works_count.

public function onFlush(OnFlushEventArgs $args)
{
    foreach ($uow->getScheduledEntityUpdates() as $work) {
        $changedTags = /* update relevant tags and return all the changed ones */

        $metadata = $em->getClassMetadata('Acme\Entity\Tag');

        foreach ($changedTags as $tag) {
            $uow->recomputeSingleEntityChangeSet($metadata, $tag);
        }
    }
}

Now if I read the changesets of the updated tags, the changes of works_count appears correctly, but they don't get updated in the database..

If I replace recomputeSingleEntityChangeSet() with computeChangeSet() then everything works as expected and the DB is updated, but computeChangeSet() has an @internal Don't call from the outside. annotation on it, so I'm not sure what the consequences are..

Every source on the internet says to use recomputeSingleEntityChangeSet so why doesn't it work in this case?

P.S The tags are managed by the EntityManager ($em->contains($tag) returns true)

Was it helpful?

Solution

This problem was related with a bug in UnitOfWork and finally it's fixed with the release of Doctrine ORM 2.4.3 on September 11, 2014. See DDC-2996 for details.

OTHER TIPS

It seems that Doctrine 2.2 can merge change sets or generate new change sets, but it needs to know which. If you get it wrong, it will either replace your existing change sets or do nothing at all. I'd be very interested to know if there is a better option than this, or if this is even right.

    if($uow->getEntityChangeSet($entity)) {
        /** If the entity has pending changes, we need to recompute/merge. */
        $uow->recomputeSingleEntityChangeSet($meta, $contact);
    } else {
        /** If there are no changes, we compute from scratch? */
        $uow->computeChangeSet($meta, $entity);
    }

In doctrine 2.4.1, use recomputeSingleEntityChangeSet only if you are changing tag in the event listener AND UOW contain tag ChangeSet (Change that happen outside of the event listener). Basically recomputeSingleEntityChangeSet is a function to merge ChangeSet for an entity.

Doc from the function The passed entity must be a managed entity. If the entity already has a change set because this method is invoked during a commit cycle then the change sets are added whereby changes detected in this method prevail.

NOTE: You need to make sure UOW already have ChangeSet for the entity, otherwise it will not merge.

For future readers, at all cost try to avoid the listeners. Those are hardly testable, your domain should not rely on magic. Consider OP's test case how to achieve the same without Doctrine events:

Work class:

public function addTag(Tag $tag): void
{
    if (!$this->tags->contains($tag)) {
        $this->tags->add($tag);
        $tag->addWork($this);
    }
}

Tag class:

public function addWork(Work $work): void
{
    if (!$this->works->contains($work)) {
        $work->addTag($this);
        $this->works->add($work);
        $this->worksCount = count($this->works);
    }
}

TagTest class:

public function testItUpdatesWorksCountWhenWorkIsAdded()
{
    $tag = new Tag();

    $tag->addWork(new Work());
    $tag->addWork(new Work());

    $this->assertSame(2, $tag->getWorkCount());
}

public function testItDoesNotUpdateWorksCountIfWorkIsAlreadyInCollection()
{
    $tag = new Tag();
    $work = new Work();
    $tag->addWork($work);

    $tag->addWork($work);

    $this->assertSame(1, $tag->getWorkCount());
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top