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

Hotfix - #1169 - extra lazy one to many must be no-op when not doing orphan removal #1288

Merged
26 changes: 10 additions & 16 deletions lib/Doctrine/ORM/Persisters/Collection/OneToManyPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,27 +157,21 @@ public function contains(PersistentCollection $collection, $element)
*/
public function removeElement(PersistentCollection $collection, $element)
{
if ( ! $this->isValidEntityState($element)) {
$mapping = $collection->getMapping();

if ( ! $mapping['orphanRemoval']) {
// no-op: this is not the owning side, therefore no operations should be applied
return false;
}

$mapping = $collection->getMapping();
$persister = $this->uow->getEntityPersister($mapping['targetEntity']);

$targetMetadata = $this->em->getClassMetadata($mapping['targetEntity']);

if ($element instanceof Proxy && ! $element->__isInitialized()) {
$element->__load();
if ( ! $this->isValidEntityState($element)) {
return false;
}

// clearing owning side value
$targetMetadata->reflFields[$mapping['mappedBy']]->setValue($element, null);

$this->uow->computeChangeSet($targetMetadata, $element);

$persister->update($element);

return true;
return $this
->uow
->getEntityPersister($mapping['targetEntity'])
->delete($element);
}

/**
Expand Down
14 changes: 13 additions & 1 deletion tests/Doctrine/Tests/Models/Tweet/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,26 @@ class User
*/
public $tweets;

/**
* @OneToMany(targetEntity="UserList", mappedBy="owner", fetch="EXTRA_LAZY", orphanRemoval=true)
*/
public $userLists;

public function __construct()
{
$this->tweets = new ArrayCollection();
$this->tweets = new ArrayCollection();
$this->userLists = new ArrayCollection();
}

public function addTweet(Tweet $tweet)
{
$tweet->setAuthor($this);
$this->tweets->add($tweet);
}

public function addUserList(UserList $userList)
{
$userList->owner = $this;
$this->userLists->add($userList);
}
}
29 changes: 29 additions & 0 deletions tests/Doctrine/Tests/Models/Tweet/UserList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Doctrine\Tests\Models\Tweet;

/**
* @Entity
* @Table(name="tweet_user_list")
*/
class UserList
{
const CLASSNAME = __CLASS__;

/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
public $id;

/**
* @Column(type="string")
*/
public $listName;

/**
* @ManyToOne(targetEntity="User", inversedBy="userLists")
*/
public $owner;
}
145 changes: 132 additions & 13 deletions tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\Tests\Models\DDC2504\DDC2504OtherClass;
use Doctrine\Tests\Models\Tweet\Tweet;
use Doctrine\Tests\Models\Tweet\User;
use Doctrine\Tests\Models\Tweet\UserList;
use Doctrine\Tests\OrmFunctionalTestCase;

/**
Expand Down Expand Up @@ -485,7 +486,7 @@ public function testRemoveElementOneToMany()

$this->assertFalse($user->articles->isInitialized(), "Post-Condition: Collection is not initialized.");
// NOTE: +2 queries because CmsArticle is a versioned entity, and that needs to be handled accordingly
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can be removed, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

$this->assertEquals($queryCount + 2, $this->getCurrentQueryCount());
$this->assertEquals($queryCount, $this->getCurrentQueryCount());

// Test One to Many removal with Entity state as new
$article = new \Doctrine\Tests\Models\CMS\CmsArticle();
Expand Down Expand Up @@ -528,29 +529,45 @@ public function testRemoveElementOneToMany()
*/
public function testRemovalOfManagedElementFromOneToManyJoinedInheritanceCollectionDoesNotInitializeIt()
{
/* @var $otherClass DDC2504OtherClass */
$otherClass = $this->_em->find(DDC2504OtherClass::CLASSNAME, $this->ddc2504OtherClassId);
/* @var $childClass DDC2504ChildClass */
$childClass = $this->_em->find(DDC2504ChildClass::CLASSNAME, $this->ddc2504ChildClassId);

$queryCount = $this->getCurrentQueryCount();

$otherClass->childClasses->removeElement($childClass);
$childClass->other = null; // updating owning side

$this->assertFalse($otherClass->childClasses->isInitialized(), 'Collection is not initialized.');

$this->assertEquals(
$queryCount + 1,
$queryCount,
$this->getCurrentQueryCount(),
'The owning side of the association is updated'
'No queries have been executed'
);

$this->assertFalse($otherClass->childClasses->contains($childClass));
$this->assertTrue(
$otherClass->childClasses->contains($childClass),
'Collection item still not updated (needs flushing)'
);

$this->_em->flush();

$this->assertFalse(
$otherClass->childClasses->contains($childClass),
'Referenced item was removed in the transaction'
);

$this->assertFalse($otherClass->childClasses->isInitialized(), 'Collection is not initialized.');
}

/**
* @group DDC-2504
*/
public function testRemovalOfNonManagedElementFromOneToManyJoinedInheritanceCollectionDoesNotInitializeIt()
{
/* @var $otherClass DDC2504OtherClass */
$otherClass = $this->_em->find(DDC2504OtherClass::CLASSNAME, $this->ddc2504OtherClassId);
$queryCount = $this->getCurrentQueryCount();

Expand All @@ -568,6 +585,7 @@ public function testRemovalOfNonManagedElementFromOneToManyJoinedInheritanceColl
*/
public function testRemovalOfNewElementFromOneToManyJoinedInheritanceCollectionDoesNotInitializeIt()
{
/* @var $otherClass DDC2504OtherClass */
$otherClass = $this->_em->find(DDC2504OtherClass::CLASSNAME, $this->ddc2504OtherClassId);
$childClass = new DDC2504ChildClass();

Expand Down Expand Up @@ -1035,7 +1053,7 @@ private function loadFixture()
/**
* @group DDC-3343
*/
public function testRemovesManagedElementFromOneToManyExtraLazyCollection()
public function testRemoveManagedElementFromOneToManyExtraLazyCollectionIsNoOp()
{
list($userId, $tweetId) = $this->loadTweetFixture();

Expand All @@ -1049,22 +1067,21 @@ public function testRemovesManagedElementFromOneToManyExtraLazyCollection()
/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$this->assertCount(0, $user->tweets);
$this->assertCount(1, $user->tweets, 'Element was not removed - need to update the owning side first');
}

/**
* @group DDC-3343
*/
public function testRemovesManagedElementFromOneToManyExtraLazyCollectionWithoutDeletingTheTargetEntityEntry()
public function testRemoveManagedElementFromOneToManyExtraLazyCollectionWithoutDeletingTheTargetEntityEntryIsNoOp()
{
list($userId, $tweetId) = $this->loadTweetFixture();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);
$tweet = $this->_em->find(Tweet::CLASSNAME, $tweetId);

$e = $this->_em->find(Tweet::CLASSNAME, $tweetId);

$user->tweets->removeElement($e);
$user->tweets->removeElement($tweet);

$this->_em->clear();

Expand All @@ -1076,7 +1093,11 @@ public function testRemovesManagedElementFromOneToManyExtraLazyCollectionWithout
'Even though the collection is extra lazy, the tweet should not have been deleted'
);

$this->assertNull($tweet->author, 'Tweet author link has been removed');
$this->assertInstanceOf(
User::CLASSNAME,
$tweet->author,
'Tweet author link has not been removed - need to update the owning side first'
);
}

/**
Expand All @@ -1102,12 +1123,89 @@ public function testRemovingManagedLazyProxyFromExtraLazyOneToManyDoesRemoveTheA
'Even though the collection is extra lazy, the tweet should not have been deleted'
);

$this->assertNull($tweet->author);
$this->assertInstanceOf(User::CLASSNAME, $tweet->author);

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$this->assertCount(1, $user->tweets, 'Element was not removed - need to update the owning side first');
}

/**
* @group DDC-3343
*/
public function testRemoveOrphanedManagedElementFromOneToManyExtraLazyCollection()
{
list($userId, $userListId) = $this->loadUserListFixture();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$user->userLists->removeElement($this->_em->find(UserList::CLASSNAME, $userListId));

$this->_em->clear();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$this->assertCount(0, $user->tweets);
$this->assertCount(0, $user->userLists, 'Element was removed from association due to orphan removal');
$this->assertNull(
$this->_em->find(UserList::CLASSNAME, $userListId),
'Element was deleted due to orphan removal'
);
}

/**
* @group DDC-3343
*/
public function testRemoveOrphanedUnManagedElementFromOneToManyExtraLazyCollection()
{
list($userId, $userListId) = $this->loadUserListFixture();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$user->userLists->removeElement(new UserList());

$this->_em->clear();

/* @var $userList UserList */
$userList = $this->_em->find(UserList::CLASSNAME, $userListId);
$this->assertInstanceOf(
UserList::CLASSNAME,
$userList,
'Even though the collection is extra lazy + orphan removal, the user list should not have been deleted'
);

$this->assertInstanceOf(
User::CLASSNAME,
$userList->owner,
'User list to owner link has not been removed'
);
}

/**
* @group DDC-3343
*/
public function testRemoveOrphanedManagedLazyProxyFromExtraLazyOneToMany()
{
list($userId, $userListId) = $this->loadUserListFixture();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$user->userLists->removeElement($this->_em->getReference(UserList::CLASSNAME, $userListId));

$this->_em->clear();

/* @var $user User */
$user = $this->_em->find(User::CLASSNAME, $userId);

$this->assertCount(0, $user->userLists, 'Element was removed from association due to orphan removal');
$this->assertNull(
$this->_em->find(UserList::CLASSNAME, $userListId),
'Element was deleted due to orphan removal'
);
}

/**
Expand All @@ -1130,4 +1228,25 @@ private function loadTweetFixture()

return array($user->id, $tweet->id);
}

/**
* @return int[] ordered tuple: user id and user list id
*/
private function loadUserListFixture()
{
$user = new User();
$userList = new UserList();

$user->name = 'ocramius';
$userList->listName = 'PHP Developers to follow closely';

$user->addUserList($userList);

$this->_em->persist($user);
$this->_em->persist($userList);
$this->_em->flush();
$this->_em->clear();

return array($user->id, $userList->id);
}
}
9 changes: 8 additions & 1 deletion tests/Doctrine/Tests/OrmFunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
),
'tweet' => array(
'Doctrine\Tests\Models\Tweet\User',
'Doctrine\Tests\Models\Tweet\Tweet'
'Doctrine\Tests\Models\Tweet\Tweet',
'Doctrine\Tests\Models\Tweet\UserList',
),
'ddc2504' => array(
'Doctrine\Tests\Models\DDC2504\DDC2504RootClass',
Expand Down Expand Up @@ -387,6 +388,12 @@ protected function tearDown()
$conn->executeUpdate('DELETE FROM taxi_driver');
}

if (isset($this->_usedModelSets['tweet'])) {
$conn->executeUpdate('DELETE FROM tweet_tweet');
$conn->executeUpdate('DELETE FROM tweet_user_list');
$conn->executeUpdate('DELETE FROM tweet_user');
}

if (isset($this->_usedModelSets['cache'])) {
$conn->executeUpdate('DELETE FROM cache_attraction_location_info');
$conn->executeUpdate('DELETE FROM cache_attraction_contact_info');
Expand Down