1
0
mirror of synced 2025-01-31 12:32:59 +03:00

Merge branch 'hotfix/#1172-avoid-proxy-initialization-when-proxy-is-merged-into-uow'

Close #1172
This commit is contained in:
Marco Pivetta 2015-01-16 22:56:03 +01:00
commit 074ec358ab
4 changed files with 347 additions and 80 deletions

View File

@ -26,7 +26,9 @@ use Doctrine\ORM\Query\ResultSetMapping;
* EntityManager interface
*
* @since 2.4
* @author Lars Strojny <lars@strojny.net
* @author Lars Strojny <lars@strojny.net>
*
* @method Mapping\ClassMetadata getClassMetadata($className)
*/
interface EntityManagerInterface extends ObjectManager
{

View File

@ -1809,11 +1809,6 @@ class UnitOfWork implements PropertyChangedListener
$managedCopy = $entity;
if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
$this->em->getProxyFactory()->resetUninitializedProxy($entity);
$entity->__load();
}
// Try to look the entity up in the identity map.
$id = $class->getIdentifierValues($entity);
@ -1853,10 +1848,6 @@ class UnitOfWork implements PropertyChangedListener
$class->setIdentifierValues($managedCopy, $id);
$this->persistNew($class, $managedCopy);
} else {
if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
$managedCopy->__load();
}
}
}
@ -1873,76 +1864,8 @@ class UnitOfWork implements PropertyChangedListener
$visited[$oid] = $managedCopy; // mark visited
// Merge state of $entity into existing (managed) entity
foreach ($class->reflClass->getProperties() as $prop) {
$name = $prop->name;
$prop->setAccessible(true);
if ( ! isset($class->associationMappings[$name])) {
if ( ! $class->isIdentifier($name)) {
$prop->setValue($managedCopy, $prop->getValue($entity));
}
} else {
$assoc2 = $class->associationMappings[$name];
if ($assoc2['type'] & ClassMetadata::TO_ONE) {
$other = $prop->getValue($entity);
if ($other === null) {
$prop->setValue($managedCopy, null);
} else if ($other instanceof Proxy && !$other->__isInitialized__) {
// do not merge fields marked lazy that have not been fetched.
continue;
} else if ( ! $assoc2['isCascadeMerge']) {
if ($this->getEntityState($other) === self::STATE_DETACHED) {
$targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
$relatedId = $targetClass->getIdentifierValues($other);
if ($targetClass->subClasses) {
$other = $this->em->find($targetClass->name, $relatedId);
} else {
$other = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $relatedId);
$this->registerManaged($other, $relatedId, array());
}
}
$prop->setValue($managedCopy, $other);
}
} else {
$mergeCol = $prop->getValue($entity);
if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
// do not merge fields marked lazy that have not been fetched.
// keep the lazy persistent collection of the managed copy.
continue;
}
$managedCol = $prop->getValue($managedCopy);
if (!$managedCol) {
$managedCol = new PersistentCollection($this->em,
$this->em->getClassMetadata($assoc2['targetEntity']),
new ArrayCollection
);
$managedCol->setOwner($managedCopy, $assoc2);
$prop->setValue($managedCopy, $managedCol);
$this->originalEntityData[$oid][$name] = $managedCol;
}
if ($assoc2['isCascadeMerge']) {
$managedCol->initialize();
// clear and set dirty a managed collection if its not also the same collection to merge from.
if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
$managedCol->unwrap()->clear();
$managedCol->setDirty(true);
if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) {
$this->scheduleForDirtyCheck($managedCopy);
}
}
}
}
}
if ($class->isChangeTrackingNotify()) {
// Just treat all properties as changed, there is no other choice.
$this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
}
if (!($entity instanceof Proxy && ! $entity->__isInitialized())) {
$this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
}
if ($class->isChangeTrackingDeferredExplicit()) {
@ -3393,6 +3316,108 @@ class UnitOfWork implements PropertyChangedListener
return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
}
/**
* @param object $entity
* @param object $managedCopy
*
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
{
$class = $this->em->getClassMetadata(get_class($entity));
foreach ($class->reflClass->getProperties() as $prop) {
$name = $prop->name;
$prop->setAccessible(true);
if ( ! isset($class->associationMappings[$name])) {
if ( ! $class->isIdentifier($name)) {
$prop->setValue($managedCopy, $prop->getValue($entity));
}
} else {
$assoc2 = $class->associationMappings[$name];
if ($assoc2['type'] & ClassMetadata::TO_ONE) {
$other = $prop->getValue($entity);
if ($other === null) {
$prop->setValue($managedCopy, null);
} else {
if ($other instanceof Proxy && !$other->__isInitialized()) {
// do not merge fields marked lazy that have not been fetched.
return;
}
if ( ! $assoc2['isCascadeMerge']) {
if ($this->getEntityState($other) === self::STATE_DETACHED) {
$targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
$relatedId = $targetClass->getIdentifierValues($other);
if ($targetClass->subClasses) {
$other = $this->em->find($targetClass->name, $relatedId);
} else {
$other = $this->em->getProxyFactory()->getProxy(
$assoc2['targetEntity'],
$relatedId
);
$this->registerManaged($other, $relatedId, array());
}
}
$prop->setValue($managedCopy, $other);
}
}
} else {
$mergeCol = $prop->getValue($entity);
if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
// do not merge fields marked lazy that have not been fetched.
// keep the lazy persistent collection of the managed copy.
return;
}
$managedCol = $prop->getValue($managedCopy);
if ( ! $managedCol) {
$managedCol = new PersistentCollection(
$this->em,
$this->em->getClassMetadata($assoc2['targetEntity']),
new ArrayCollection
);
$managedCol->setOwner($managedCopy, $assoc2);
$prop->setValue($managedCopy, $managedCol);
$this->originalEntityData[spl_object_hash($entity)][$name] = $managedCol;
}
if ($assoc2['isCascadeMerge']) {
$managedCol->initialize();
// clear and set dirty a managed collection if its not also the same collection to merge from.
if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
$managedCol->unwrap()->clear();
$managedCol->setDirty(true);
if ($assoc2['isOwningSide']
&& $assoc2['type'] == ClassMetadata::MANY_TO_MANY
&& $class->isChangeTrackingNotify()
) {
$this->scheduleForDirtyCheck($managedCopy);
}
}
}
}
}
if ($class->isChangeTrackingNotify()) {
// Just treat all properties as changed, there is no other choice.
$this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
}
}
}
/**
* This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
* Unit of work able to fire deferred events, related to loading events here.

View File

@ -8,6 +8,8 @@ namespace Doctrine\Tests\Models\Generic;
*/
class DateTimeModel
{
const CLASSNAME = __CLASS__;
/**
* @Id @Column(type="integer")
* @GeneratedValue

View File

@ -0,0 +1,238 @@
<?php
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Logging\DebugStack;
use Doctrine\DBAL\Logging\SQLLogger;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
use Doctrine\Tests\Models\Generic\DateTimeModel;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\TestUtil;
class MergeProxiesTest extends OrmFunctionalTestCase
{
/**
* {@inheritDoc}
*/
protected function setUp()
{
parent::setUp();
try {
$this->_schemaTool->createSchema(array($this->_em->getClassMetadata(DateTimeModel::CLASSNAME)));
} catch (ToolsException $ignored) {
}
}
/**
* @group DDC-1392
* @group DDC-1734
* @group DDC-3368
* @group #1172
*/
public function testMergeDetachedUnInitializedProxy()
{
$detachedUninitialized = $this->_em->getReference(DateTimeModel::CLASSNAME, 123);
$this->_em->clear();
$managed = $this->_em->getReference(DateTimeModel::CLASSNAME, 123);
$this->assertSame($managed, $this->_em->merge($detachedUninitialized));
$this->assertFalse($managed->__isInitialized());
$this->assertFalse($detachedUninitialized->__isInitialized());
}
/**
* @group DDC-1392
* @group DDC-1734
* @group DDC-3368
* @group #1172
*/
public function testMergeUnserializedUnInitializedProxy()
{
$detachedUninitialized = $this->_em->getReference(DateTimeModel::CLASSNAME, 123);
$this->_em->clear();
$managed = $this->_em->getReference(DateTimeModel::CLASSNAME, 123);
$this->assertSame(
$managed,
$this->_em->merge(unserialize(serialize($this->_em->merge($detachedUninitialized))))
);
$this->assertFalse($managed->__isInitialized());
$this->assertFalse($detachedUninitialized->__isInitialized());
}
/**
* @group DDC-1392
* @group DDC-1734
* @group DDC-3368
* @group #1172
*/
public function testMergeManagedProxy()
{
$managed = $this->_em->getReference(DateTimeModel::CLASSNAME, 123);
$this->assertSame($managed, $this->_em->merge($managed));
$this->assertFalse($managed->__isInitialized());
}
/**
* @group DDC-1392
* @group DDC-1734
* @group DDC-3368
* @group #1172
*/
public function testMergingProxyFromDifferentEntityManagerWithExistingManagedInstanceDoesNotReplaceInitializer()
{
$em1 = $this->createEntityManager($logger1 = new DebugStack());
$em2 = $this->createEntityManager($logger2 = new DebugStack());
$file1 = new DateTimeModel();
$file2 = new DateTimeModel();
$em1->persist($file1);
$em2->persist($file2);
$em1->flush();
$em2->flush();
$em1->clear();
$em2->clear();
$queryCount1 = count($logger1->queries);
$queryCount2 = count($logger2->queries);
$proxy1 = $em1->getReference(DateTimeModel::CLASSNAME, $file1->id);
$proxy2 = $em2->getReference(DateTimeModel::CLASSNAME, $file1->id);
$merged2 = $em2->merge($proxy1);
$this->assertNotSame($proxy1, $merged2);
$this->assertSame($proxy2, $merged2);
$this->assertFalse($proxy1->__isInitialized());
$this->assertFalse($proxy2->__isInitialized());
$proxy1->__load();
$this->assertCount(
$queryCount1 + 1,
$logger1->queries,
'Loading the first proxy was done through the first entity manager'
);
$this->assertCount(
$queryCount2,
$logger2->queries,
'No queries were executed on the second entity manager, as it is unrelated with the first proxy'
);
$proxy2->__load();
$this->assertCount(
$queryCount1 + 1,
$logger1->queries,
'Loading the second proxy does not affect the first entity manager'
);
$this->assertCount(
$queryCount2 + 1,
$logger2->queries,
'Loading of the second proxy instance was done through the second entity manager'
);
}
/**
* @group DDC-1392
* @group DDC-1734
* @group DDC-3368
* @group #1172
*/
public function testMergingUnInitializedProxyDoesNotInitializeIt()
{
$em1 = $this->createEntityManager($logger1 = new DebugStack());
$em2 = $this->createEntityManager($logger2 = new DebugStack());
$file1 = new DateTimeModel();
$file2 = new DateTimeModel();
$em1->persist($file1);
$em2->persist($file2);
$em1->flush();
$em2->flush();
$em1->clear();
$em2->clear();
$queryCount1 = count($logger1->queries);
$queryCount2 = count($logger1->queries);
$unManagedProxy = $em1->getReference(DateTimeModel::CLASSNAME, $file1->id);
$mergedInstance = $em2->merge($unManagedProxy);
$this->assertNotInstanceOf('Doctrine\Common\Proxy\Proxy', $mergedInstance);
$this->assertNotSame($unManagedProxy, $mergedInstance);
$this->assertFalse($unManagedProxy->__isInitialized());
$this->assertCount(
$queryCount1,
$logger1->queries,
'Loading the merged instance affected only the first entity manager'
);
$this->assertCount(
$queryCount1 + 1,
$logger2->queries,
'Loading the merged instance was done via the second entity manager'
);
$unManagedProxy->__load();
$this->assertCount(
$queryCount1 + 1,
$logger1->queries,
'Loading the first proxy was done through the first entity manager'
);
$this->assertCount(
$queryCount2 + 1,
$logger2->queries,
'No queries were executed on the second entity manager, as it is unrelated with the first proxy'
);
}
/**
* @param SQLLogger $logger
*
* @return EntityManager
*/
private function createEntityManager(SQLLogger $logger)
{
$config = new Configuration();
$config->setProxyDir(realpath(__DIR__ . '/../../Proxies'));
$config->setProxyNamespace('Doctrine\Tests\Proxies');
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(
array(realpath(__DIR__ . '/../../Models/Cache')),
true
));
$connection = TestUtil::getConnection();
$connection->getConfiguration()->setSQLLogger($logger);
$entityManager = EntityManager::create($connection, $config);
try {
(new SchemaTool($entityManager))
->createSchema([$this->_em->getClassMetadata(DateTimeModel::CLASSNAME)]);
} catch (ToolsException $ignored) {
// tables were already created
}
return $entityManager;
}
}