Don't load detached proxies when merging them.
Ticket DDC-1392 fixed an issue where uninitialized proxies could not be merged because the merge routine couldn't get the identifier from them. The soution was to initialize the proxy. Ticket DDC-1734 fixed the merging of *unserialized* uninitialized proxies by resetting their internals, so these proxies were able to initialize, as required by the fix for DDC-1392. Somehow, in the meanwhile, the fix for DDC-1392 is not needed anymore: reverting the patch will not break the associated test (but it does break the test for DDC-1734). This means it is not needed anymore to initialize the proxy when merging. Uninitialized proxies that get merged should not be loaded at all. Since they are not initialized, the entity data for sure hasn't changed, so it can be safely ignored. Actually, the only thing the data is needed for while merging, is to copy it into the managed entity, but that one is already supposed to be up to date. By not initializing the proxy, a potential database roundtrip is saved, and the fix for DDC-1734 is not needed anymore. Besides optimizing the merge, this patch also solves an issue with merging. Currently, when a detached uninitialized proxy is merged while there is already a corresponding managed entity (proxy or not), the ORM returns a blank entity instead of returning the already managed entity. This patch makes sure that already existing managed entities are re-used.
This commit is contained in:
parent
f28654de12
commit
ec35d4886c
@ -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);
|
||||
|
||||
@ -1873,76 +1868,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 +3320,100 @@ class UnitOfWork implements PropertyChangedListener
|
||||
return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param object $managedCopy
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
* @internal param ClassMetadata $class
|
||||
*/
|
||||
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);
|
||||
$oid = spl_object_hash($entity);
|
||||
$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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
|
||||
use Doctrine\ORM\Tools\ToolsException;
|
||||
|
||||
class MergeUninitializedProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase {
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
try {
|
||||
$this->_schemaTool->createSchema(array(
|
||||
$this->_em->getClassMetadata(__NAMESPACE__ . '\MUPFile'),
|
||||
$this->_em->getClassMetadata(__NAMESPACE__ . '\MUPPicture'),
|
||||
));
|
||||
} catch (ToolsException $ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public function testMergeUnserializedIntoEntity() {
|
||||
|
||||
$file = new MUPFile;
|
||||
|
||||
$picture = new MUPPicture;
|
||||
$picture->file = $file;
|
||||
|
||||
$em = $this->_em;
|
||||
$em->persist($picture);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
$fileId = $file->fileId;
|
||||
$pictureId = $picture->pictureId;
|
||||
|
||||
$picture = $em->find(__NAMESPACE__ . '\MUPPicture', $pictureId);
|
||||
$serializedPicture = serialize($picture);
|
||||
|
||||
$em->clear();
|
||||
|
||||
$file = $em->find(__NAMESPACE__ . '\MUPFile', $fileId);
|
||||
$picture = unserialize($serializedPicture);
|
||||
$picture = $em->merge($picture);
|
||||
|
||||
$this->assertEquals($file, $picture->file, "Unserialized proxy was not merged into managed entity");
|
||||
}
|
||||
|
||||
public function testMergeDetachedIntoEntity() {
|
||||
|
||||
$file = new MUPFile;
|
||||
|
||||
$picture = new MUPPicture;
|
||||
$picture->file = $file;
|
||||
|
||||
$em = $this->_em;
|
||||
$em->persist($picture);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
$fileId = $file->fileId;
|
||||
$pictureId = $picture->pictureId;
|
||||
|
||||
$picture = $em->find(__NAMESPACE__ . '\MUPPicture', $pictureId);
|
||||
|
||||
$em->clear();
|
||||
|
||||
$file = $em->find(__NAMESPACE__ . '\MUPFile', $fileId);
|
||||
$picture = $em->merge($picture);
|
||||
|
||||
$this->assertEquals($file, $picture->file, "Detached proxy was not merged into managed entity");
|
||||
}
|
||||
|
||||
public function testMergeUnserializedIntoProxy() {
|
||||
|
||||
$file = new MUPFile;
|
||||
|
||||
$picture = new MUPPicture;
|
||||
$picture->file = $file;
|
||||
|
||||
$picture2 = new MUPPicture;
|
||||
$picture2->file = $file;
|
||||
|
||||
$em = $this->_em;
|
||||
$em->persist($picture);
|
||||
$em->persist($picture2);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
$pictureId = $picture->pictureId;
|
||||
$picture2Id = $picture2->pictureId;
|
||||
|
||||
$picture = $em->find(__NAMESPACE__ . '\MUPPicture', $pictureId);
|
||||
$serializedPicture = serialize($picture);
|
||||
|
||||
$em->clear();
|
||||
|
||||
$picture2 = $em->find(__NAMESPACE__ . '\MUPPicture', $picture2Id);
|
||||
$picture = unserialize($serializedPicture);
|
||||
$picture = $em->merge($picture);
|
||||
|
||||
$this->assertEquals($picture2->file, $picture->file, "Unserialized proxy was not merged into managed proxy");
|
||||
}
|
||||
|
||||
public function testMergeDetachedIntoProxy() {
|
||||
|
||||
$file = new MUPFile;
|
||||
|
||||
$picture = new MUPPicture;
|
||||
$picture->file = $file;
|
||||
|
||||
$picture2 = new MUPPicture;
|
||||
$picture2->file = $file;
|
||||
|
||||
$em = $this->_em;
|
||||
$em->persist($picture);
|
||||
$em->persist($picture2);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
$pictureId = $picture->pictureId;
|
||||
$picture2Id = $picture2->pictureId;
|
||||
|
||||
$picture = $em->find(__NAMESPACE__ . '\MUPPicture', $pictureId);
|
||||
|
||||
$em->clear();
|
||||
|
||||
$picture2 = $em->find(__NAMESPACE__ . '\MUPPicture', $picture2Id);
|
||||
$picture = $em->merge($picture);
|
||||
|
||||
$this->assertEquals($picture2->file, $picture->file, "Detached proxy was not merged into managed proxy");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class MUPPicture
|
||||
{
|
||||
/**
|
||||
* @Column(name="picture_id", type="integer")
|
||||
* @Id @GeneratedValue
|
||||
*/
|
||||
public $pictureId;
|
||||
|
||||
/**
|
||||
* @ManyToOne(targetEntity="MUPFile", cascade={"persist", "merge"})
|
||||
* @JoinColumn(name="file_id", referencedColumnName="file_id")
|
||||
*/
|
||||
public $file;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class MUPFile
|
||||
{
|
||||
/**
|
||||
* @Column(name="file_id", type="integer")
|
||||
* @Id
|
||||
* @GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
public $fileId;
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user