Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.0] Single entity flush #1139

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 54 additions & 20 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ class UnitOfWork implements PropertyChangedListener
*/
private $entityInsertions = array();

/**
* A list of all pending entity insertions, that can be avoided if we call
* a flush() method with a non related entity.
* @var array
*/
private $entityShouldBeInserted = array();

/**
* Used to detect if a flush is in progress.
* @see UnitOfWork::scheduleForInsert() method.
* @var boolean
*/
private $flushing = false;


/**
* A list of all pending entity updates.
*
Expand Down Expand Up @@ -321,6 +336,7 @@ public function __construct(EntityManagerInterface $em)
*/
public function commit($entity = null)
{
$this->flushing = true;
// Raise preFlush
if ($this->evm->hasListeners(Events::preFlush)) {
$this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
Expand All @@ -345,7 +361,7 @@ public function commit($entity = null)
$this->orphanRemovals)) {
$this->dispatchOnFlushEvent();
$this->dispatchPostFlushEvent();

$this->flushing = false;
return; // Nothing to do.
}

Expand Down Expand Up @@ -404,6 +420,7 @@ public function commit($entity = null)
$conn->rollBack();

$this->afterTransactionRolledBack();
$this->flushing = false;

throw $e;
}
Expand All @@ -428,6 +445,7 @@ public function commit($entity = null)
$this->visitedCollections =
$this->scheduledForSynchronization =
$this->orphanRemovals = array();
$this->flushing = false;
}

/**
Expand Down Expand Up @@ -471,8 +489,13 @@ private function computeSingleEntityChangeSet($entity)
if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
$this->persist($entity);
}
$oid = spl_object_hash($entity);

// Compute changes for INSERTed entities first. This must always happen even in this case.
if (isset($this->entityShouldBeInserted[$oid])) {
$this->entityInsertions[$oid] = $entity;
unset($this->entityShouldBeInserted[$oid]);
}
$this->computeScheduleInsertsChangeSets();

if ($class->isReadOnly) {
Expand All @@ -485,8 +508,6 @@ private function computeSingleEntityChangeSet($entity)
}

// Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
$oid = spl_object_hash($entity);

if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
$this->computeChangeSet($class, $entity);
}
Expand Down Expand Up @@ -754,6 +775,9 @@ public function computeChangeSet(ClassMetadata $class, $entity)
public function computeChangeSets()
{
// Compute changes for INSERTed entities first. This must always happen.
$this->entityInsertions = $this->entityInsertions + $this->entityShouldBeInserted;
$this->entityShouldBeInserted = array();

$this->computeScheduleInsertsChangeSets();

// Compute changes for other MANAGED entities. Change tracking policies take effect here.
Expand Down Expand Up @@ -966,7 +990,7 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
if ($changeSet) {
if (isset($this->entityChangeSets[$oid])) {
$this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
} else if ( ! isset($this->entityInsertions[$oid])) {
} else if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityShouldBeInserted[$oid])) {
$this->entityChangeSets[$oid] = $changeSet;
$this->entityUpdates[$oid] = $entity;
}
Expand Down Expand Up @@ -996,7 +1020,7 @@ private function executeInserts($class)

$persister->addInsert($entity);

unset($this->entityInsertions[$oid]);
unset($this->entityInsertions[$oid], $this->entityShouldBeInserted[$oid]);

if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$entities[] = $entity;
Expand Down Expand Up @@ -1202,15 +1226,19 @@ public function scheduleForInsert($entity)
if (isset($this->entityDeletions[$oid])) {
throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
}
if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid]) && !isset($this->entityShouldBeInserted[$oid])) {
throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
}

if (isset($this->entityInsertions[$oid])) {
if (isset($this->entityInsertions[$oid]) || isset($this->entityShouldBeInserted[$oid])) {
throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
}

$this->entityInsertions[$oid] = $entity;
if ($this->flushing) {
$this->entityInsertions[$oid] = $entity;
} else {
$this->entityShouldBeInserted[$oid] = $entity;
}

if (isset($this->entityIdentifiers[$oid])) {
$this->addToIdentityMap($entity);
Expand All @@ -1230,7 +1258,7 @@ public function scheduleForInsert($entity)
*/
public function isScheduledForInsert($entity)
{
return isset($this->entityInsertions[spl_object_hash($entity)]);
return isset($this->entityInsertions[spl_object_hash($entity)]) || isset($this->entityShouldBeInserted[spl_object_hash($entity)]);
}

/**
Expand All @@ -1254,7 +1282,7 @@ public function scheduleForUpdate($entity)
throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
}

if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid]) && ! isset($this->entityShouldBeInserted[$oid])) {
$this->entityUpdates[$oid] = $entity;
}
}
Expand Down Expand Up @@ -1327,12 +1355,12 @@ public function scheduleForDelete($entity)
{
$oid = spl_object_hash($entity);

if (isset($this->entityInsertions[$oid])) {
if (isset($this->entityInsertions[$oid]) || isset($this->entityShouldBeInserted[$oid])) {
if ($this->isInIdentityMap($entity)) {
$this->removeFromIdentityMap($entity);
}

unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
unset($this->entityInsertions[$oid], $this->entityShouldBeInserted[$oid], $this->entityStates[$oid]);

return; // entity has not been persisted yet, so nothing more to do.
}
Expand Down Expand Up @@ -1377,7 +1405,8 @@ public function isEntityScheduled($entity)

return isset($this->entityInsertions[$oid])
|| isset($this->entityUpdates[$oid])
|| isset($this->entityDeletions[$oid]);
|| isset($this->entityDeletions[$oid])
|| isset($this->entityShouldBeInserted[$oid]);
}

/**
Expand Down Expand Up @@ -1984,7 +2013,8 @@ private function doDetach($entity, array &$visited, $noCascade = false)
$this->entityDeletions[$oid],
$this->entityIdentifiers[$oid],
$this->entityStates[$oid],
$this->originalEntityData[$oid]
$this->originalEntityData[$oid],
$this->entityShouldBeInserted[$oid]
);
break;
case self::STATE_NEW:
Expand Down Expand Up @@ -2040,12 +2070,15 @@ private function doRefresh($entity, array &$visited)
throw ORMInvalidArgumentException::entityNotManaged($entity);
}

$this->getEntityPersister($class->name)->refresh(
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
$entity
);
if (isset($this->entityIdentifiers[$oid])) {
$this->getEntityPersister($class->name)->refresh(
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
$entity
);

$this->cascadeRefresh($entity, $visited);
}

$this->cascadeRefresh($entity, $visited);
}

/**
Expand Down Expand Up @@ -2376,6 +2409,7 @@ public function clear($entityName = null)
$this->entityChangeSets =
$this->entityStates =
$this->scheduledForSynchronization =
$this->entityShouldBeInserted =
$this->entityInsertions =
$this->entityUpdates =
$this->entityDeletions =
Expand Down Expand Up @@ -2964,7 +2998,7 @@ public function scheduleForDirtyCheck($entity)
*/
public function hasPendingInsertions()
{
return ! empty($this->entityInsertions);
return ! empty($this->entityInsertions) || !empty($this->entityShouldBeInserted);
}

/**
Expand Down
62 changes: 56 additions & 6 deletions tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\Models\CMS\CmsComment;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\Models\CMS\CmsEmail;

class BasicFunctionalTest extends OrmFunctionalTestCase
{
Expand Down Expand Up @@ -1084,8 +1085,9 @@ public function testFlushManyExplicitEntities()

$this->assertTrue($userA->id > 0, 'user a has an id');
$this->assertTrue($userB->id > 0, 'user b has an id');
$this->assertTrue($userC->id > 0, 'user c has an id');
$this->assertEquals('UserC', $userC->name, 'name has not changed because we did not flush it');
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($userC), 'user c is not flushed');

$this->assertEquals('changed name', $userC->name, 'name changed because we did not flush it, so we can not refresh it');
}

/**
Expand Down Expand Up @@ -1149,7 +1151,7 @@ public function testFlushSingleAndNewEntity()
$this->_em->flush($user);

$this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager");
$this->assertTrue($otherUser->id > 0, "other user has an id");
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($otherUser), 'other user is not flushed');
}

/**
Expand All @@ -1175,8 +1177,56 @@ public function testFlushAndCascadePersist()

$this->_em->flush($user);

$this->assertTrue($this->_em->contains($address), "Other user is contained in EntityManager");
$this->assertTrue($address->id > 0, "other user has an id");
$this->assertTrue($this->_em->contains($address), "Address is contained in EntityManager");
$this->assertTrue($address->id > 0, "Address has an id");
}

public function testFlushOnSingleEntity()
{
$user = new CmsUser;
$user->name = 'Dominik';
$user->username = 'domnikl';
$user->status = 'developer';

$this->_em->persist($user);

$email = new CmsEmail;
$email->email = '[email protected]';

$this->_em->persist($email);

$q = $this->getCurrentQueryCount();
$this->_em->flush($email);

$this->assertEquals($q+3, $this->getCurrentQueryCount());

$q = $this->getCurrentQueryCount();
$this->_em->flush($user);

$this->assertEquals($q+3, $this->getCurrentQueryCount());
}

public function testFlushOnSingleEntitySameClass()
{
$domnik = new CmsUser;
$domnik->name = 'Dominik';
$domnik->username = 'domnikl';
$domnik->status = 'developer';
$this->_em->persist($domnik);

$fabian = new CmsUser;
$fabian->name = 'Fabian';
$fabian->username = 'fabian';
$fabian->status = 'tester';
$this->_em->persist($fabian);

$q = $this->getCurrentQueryCount();
$this->_em->flush($fabian);
$this->assertEquals($q+3, $this->getCurrentQueryCount(), "Only Fabian should be flushed");

$q = $this->getCurrentQueryCount();
$this->_em->flush($domnik);
$this->assertEquals($q+3, $this->getCurrentQueryCount(), "Dominik should be flushed");
}

/**
Expand Down Expand Up @@ -1253,7 +1303,7 @@ public function testProxyIsIgnored()
$this->_em->flush($user);

$this->assertTrue($this->_em->contains($otherUser), "Other user is contained in EntityManager");
$this->assertTrue($otherUser->id > 0, "other user has an id");
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($otherUser), 'other user is not flushed');
}

/**
Expand Down