Merge branch 'hotfix/#1172-avoid-proxy-initialization-when-proxy-is-merged-into-uow'
Close #1172
This commit is contained in:
commit
074ec358ab
@ -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
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -8,6 +8,8 @@ namespace Doctrine\Tests\Models\Generic;
|
||||
*/
|
||||
class DateTimeModel
|
||||
{
|
||||
const CLASSNAME = __CLASS__;
|
||||
|
||||
/**
|
||||
* @Id @Column(type="integer")
|
||||
* @GeneratedValue
|
||||
|
238
tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php
Normal file
238
tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php
Normal 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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user