diff --git a/UPGRADE-1.3.md b/UPGRADE-1.3.md index f1eaadb36a..22cc897366 100644 --- a/UPGRADE-1.3.md +++ b/UPGRADE-1.3.md @@ -86,6 +86,13 @@ cause an exception in 2.0. It is possible to have multiple fields with the same name in the database as long as all but one of them have the `notSaved` option set. +## Persisters + + * The `delete` and `update` methods in + `Doctrine\ODM\MongoDB\Persisters\CollectionPersister` are deprecated. Use + `deleteAll` and `updateAll` instead. The method signatures will be adapted + to match those of `deleteAll` and `updateAll` in 2.0. + ## Proxies * The usage of proxies from Doctrine Common was deprecated and will be replaced diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php index 687dc6ee53..fb1c6cfbd8 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php @@ -19,12 +19,31 @@ namespace Doctrine\ODM\MongoDB\Persisters; +use Closure; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; +use UnexpectedValueException; +use const E_USER_DEPRECATED; +use function array_diff_key; +use function array_fill_keys; +use function array_flip; +use function array_intersect_key; +use function array_keys; +use function array_map; +use function array_reverse; +use function array_values; +use function count; +use function end; +use function get_class; +use function implode; +use function sort; +use function sprintf; +use function strpos; +use function trigger_error; /** * The CollectionPersister is responsible for persisting collections of embedded @@ -40,11 +59,7 @@ */ class CollectionPersister { - /** - * The DocumentManager instance. - * - * @var DocumentManager - */ + /** @var DocumentManager */ private $dm; /** @@ -68,14 +83,50 @@ public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfW $this->uow = $uow; } + /** + * Deletes a PersistentCollection instances completely from a document using $unset. + * + * @param object $parent + * @param PersistentCollectionInterface[] $collections + * @param array $options + */ + public function deleteAll($parent, array $collections, array $options) + { + $unsetPathsMap = []; + + foreach ($collections as $collection) { + $mapping = $collection->getMapping(); + if ($mapping['isInverseSide']) { + continue; // ignore inverse side + } + if (CollectionHelper::isAtomic($mapping['strategy'])) { + throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker'); + } + list($propertyPath) = $this->getPathAndParent($collection); + $unsetPathsMap[$propertyPath] = true; + } + + if (empty($unsetPathsMap)) { + return; + } + + $unsetPaths = array_fill_keys($this->excludeSubPaths(array_keys($unsetPathsMap)), true); + $query = ['$unset' => $unsetPaths]; + $this->executeQuery($parent, $query, $options); + } + /** * Deletes a PersistentCollection instance completely from a document using $unset. * * @param PersistentCollectionInterface $coll * @param array $options + * + * @deprecated This method will be replaced with the deleteAll method */ public function delete(PersistentCollectionInterface $coll, array $options) { + @trigger_error(sprintf('The "%s" method is deprecated and will be changed to the signature of deleteAll in 2.0.', __METHOD__), E_USER_DEPRECATED); + $mapping = $coll->getMapping(); if ($mapping['isInverseSide']) { return; // ignore inverse side @@ -94,9 +145,13 @@ public function delete(PersistentCollectionInterface $coll, array $options) * * @param PersistentCollectionInterface $coll * @param array $options + * + * @deprecated This method will be replaced with the updateAll method */ public function update(PersistentCollectionInterface $coll, array $options) { + @trigger_error(sprintf('The "%s" method is deprecated and will be changed to the signature of updateAll in 2.0.', __METHOD__), E_USER_DEPRECATED); + $mapping = $coll->getMapping(); if ($mapping['isInverseSide']) { @@ -125,6 +180,55 @@ public function update(PersistentCollectionInterface $coll, array $options) } } + /** + * Updates a list PersistentCollection instances deleting removed rows and inserting new rows. + * + * @param object $parent + * @param PersistentCollectionInterface[] $collections + * @param array $options + */ + public function updateAll($parent, array $collections, array $options) + { + $setStrategyColls = []; + $addPushStrategyColls = []; + + foreach ($collections as $coll) { + $mapping = $coll->getMapping(); + + if ($mapping['isInverseSide']) { + continue; // ignore inverse side + } + switch ($mapping['strategy']) { + case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET: + case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY: + throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker'); + + case ClassMetadata::STORAGE_STRATEGY_SET: + case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY: + $setStrategyColls[] = $coll; + break; + + case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET: + case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL: + $addPushStrategyColls[] = $coll; + break; + + default: + throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']); + } + } + + if (! empty($setStrategyColls)) { + $this->setCollections($parent, $setStrategyColls, $options); + } + if (empty($addPushStrategyColls)) { + return; + } + + $this->deleteCollections($parent, $addPushStrategyColls, $options); + $this->insertCollections($parent, $addPushStrategyColls, $options); + } + /** * Sets a PersistentCollection instance. * @@ -146,6 +250,49 @@ private function setCollection(PersistentCollectionInterface $coll, array $optio $this->executeQuery($parent, $query, $options); } + /** + * Sets a list of PersistentCollection instances. + * + * This method is intended to be used with the "set" or "setArray" + * strategies. The "setArray" strategy will ensure that the collection is + * set as a BSON array, which means the collection elements will be + * reindexed numerically before storage. + * + * @param object $parent + * @param PersistentCollectionInterface[] $collections + * @param array $options + */ + private function setCollections($parent, array $collections, array $options) + { + $pathCollMap = []; + $paths = []; + foreach ($collections as $coll) { + list($propertyPath) = $this->getPathAndParent($coll); + $pathCollMap[$propertyPath] = $coll; + $paths[] = $propertyPath; + } + + $paths = $this->excludeSubPaths($paths); + /** @var PersistentCollectionInterface[] $setColls */ + $setColls = array_intersect_key($pathCollMap, array_flip($paths)); + $setPayload = []; + foreach ($setColls as $propertyPath => $coll) { + $coll->initialize(); + $mapping = $coll->getMapping(); + $setData = $this->pb->prepareAssociatedCollectionValue( + $coll, + CollectionHelper::usesSet($mapping['strategy']) + ); + $setPayload[$propertyPath] = $setData; + } + if (empty($setPayload)) { + return; + } + + $query = ['$set' => $setPayload]; + $this->executeQuery($parent, $query, $options); + } + /** * Deletes removed elements from a PersistentCollection instance. * @@ -182,6 +329,66 @@ private function deleteElements(PersistentCollectionInterface $coll, array $opti $this->executeQuery($parent, array('$pull' => array($propertyPath => null)), $options); } + /** + * Deletes removed elements from a list of PersistentCollection instances. + * + * This method is intended to be used with the "pushAll" and "addToSet" strategies. + * + * @param object $parent + * @param PersistentCollectionInterface[] $collections + * @param array $options + */ + private function deleteCollections($parent, array $collections, array $options) + { + $pathCollMap = []; + $paths = []; + $deleteDiffMap = []; + + foreach ($collections as $coll) { + $coll->initialize(); + if (! $this->uow->isCollectionScheduledForUpdate($coll)) { + continue; + } + $deleteDiff = $coll->getDeleteDiff(); + + if (empty($deleteDiff)) { + continue; + } + list($propertyPath) = $this->getPathAndParent($coll); + + $pathCollMap[$propertyPath] = $coll; + $paths[] = $propertyPath; + $deleteDiffMap[$propertyPath] = $deleteDiff; + } + + $paths = $this->excludeSubPaths($paths); + $deleteColls = array_intersect_key($pathCollMap, array_flip($paths)); + $unsetPayload = []; + $pullPayload = []; + foreach ($deleteColls as $propertyPath => $coll) { + $deleteDiff = $deleteDiffMap[$propertyPath]; + foreach ($deleteDiff as $key => $document) { + $unsetPayload[$propertyPath . '.' . $key] = true; + } + $pullPayload[$propertyPath] = null; + } + + if (! empty($unsetPayload)) { + $this->executeQuery($parent, ['$unset' => $unsetPayload], $options); + } + if (empty($pullPayload)) { + return; + } + + /** + * @todo This is a hack right now because we don't have a proper way to + * remove an element from an array by its key. Unsetting the key results + * in the element being left in the array as null so we have to pull + * null values. + */ + $this->executeQuery($parent, ['$pull' => $pullPayload], $options); + } + /** * Inserts new elements for a PersistentCollection instance. * @@ -227,6 +434,161 @@ private function insertElements(PersistentCollectionInterface $coll, array $opti $this->executeQuery($parent, $query, $options); } + /** + * Inserts new elements for a list of PersistentCollection instances. + * + * This method is intended to be used with the "pushAll" and "addToSet" strategies. + * + * @param object $parent + * @param PersistentCollectionInterface[] $collections + * @param array $options + */ + private function insertCollections($parent, array $collections, array $options) + { + $pushAllPathCollMap = []; + $addToSetPathCollMap = []; + $pushAllPaths = []; + $addToSetPaths = []; + $diffsMap = []; + + foreach ($collections as $coll) { + $coll->initialize(); + if (! $this->uow->isCollectionScheduledForUpdate($coll)) { + continue; + } + $insertDiff = $coll->getInsertDiff(); + + if (empty($insertDiff)) { + continue; + } + + $mapping = $coll->getMapping(); + $strategy = $mapping['strategy']; + + list($propertyPath) = $this->getPathAndParent($coll); + $diffsMap[$propertyPath] = $insertDiff; + + switch ($strategy) { + case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL: + $pushAllPathCollMap[$propertyPath] = $coll; + $pushAllPaths[] = $propertyPath; + break; + + case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET: + $addToSetPathCollMap[$propertyPath] = $coll; + $addToSetPaths[] = $propertyPath; + break; + + default: + throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections'); + } + } + + if (! empty($pushAllPaths)) { + $this->pushAllCollections( + $parent, + $pushAllPaths, + $pushAllPathCollMap, + $diffsMap, + $options + ); + } + if (empty($addToSetPaths)) { + return; + } + + $this->addToSetCollections( + $parent, + $addToSetPaths, + $addToSetPathCollMap, + $diffsMap, + $options + ); + } + + /** + * Perform collections update for 'pushAll' strategy. + * + * @param object $parent Parent object to which passed collections is belong. + * @param array $collsPaths Paths of collections that is passed. + * @param array $pathCollsMap List of collections indexed by their paths. + * @param array $diffsMap List of collection diffs indexed by collections paths. + * @param array $options + */ + private function pushAllCollections($parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) + { + $pushAllPaths = $this->excludeSubPaths($collsPaths); + /** @var PersistentCollectionInterface[] $pushAllColls */ + $pushAllColls = array_intersect_key($pathCollsMap, array_flip($pushAllPaths)); + $pushAllPayload = []; + foreach ($pushAllColls as $propertyPath => $coll) { + $callback = $this->getValuePrepareCallback($coll); + $value = array_values(array_map($callback, $diffsMap[$propertyPath])); + $pushAllPayload[$propertyPath] = ['$each' => $value]; + } + + if (! empty($pushAllPayload)) { + $this->executeQuery($parent, ['$push' => $pushAllPayload], $options); + } + + $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths)); + foreach ($pushAllColls as $propertyPath => $coll) { + $callback = $this->getValuePrepareCallback($coll); + $value = array_values(array_map($callback, $diffsMap[$propertyPath])); + $query = ['$push' => [$propertyPath => ['$each' => $value]]]; + $this->executeQuery($parent, $query, $options); + } + } + + /** + * Perform collections update by 'addToSet' strategy. + * + * @param object $parent Parent object to which passed collections is belong. + * @param array $collsPaths Paths of collections that is passed. + * @param array $pathCollsMap List of collections indexed by their paths. + * @param array $diffsMap List of collection diffs indexed by collections paths. + * @param array $options + */ + private function addToSetCollections($parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) + { + $addToSetPaths = $this->excludeSubPaths($collsPaths); + /** @var PersistentCollectionInterface[] $addToSetColls */ + $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths)); + + $addToSetPayload = []; + foreach ($addToSetColls as $propertyPath => $coll) { + $callback = $this->getValuePrepareCallback($coll); + $value = array_values(array_map($callback, $diffsMap[$propertyPath])); + $addToSetPayload[$propertyPath] = ['$each' => $value]; + } + + if (empty($addToSetPayload)) { + return; + } + + $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options); + } + + /** + * Return callback instance for specified collection. This callback will prepare values for query from documents + * that collection contain. + * + * @return Closure + */ + private function getValuePrepareCallback(PersistentCollectionInterface $coll) + { + $mapping = $coll->getMapping(); + if (isset($mapping['embedded'])) { + return function ($v) use ($mapping) { + return $this->pb->prepareEmbeddedDocumentValue($mapping, $v); + }; + } + + return function ($v) use ($mapping) { + return $this->pb->prepareReferencedDocumentValue($mapping, $v); + }; + } + /** * Gets the parent information for a given PersistentCollection. It will * retrieve the top-level persistent Document that the PersistentCollection @@ -284,4 +646,29 @@ private function executeQuery($document, array $newObj, array $options) throw LockException::lockFailed($document); } } + + /** + * Remove from passed paths list all sub-paths. + * + * @param string[] $paths + * + * @return string[] + */ + private function excludeSubPaths(array $paths) + { + if (empty($paths)) { + return $paths; + } + sort($paths); + $uniquePaths = [$paths[0]]; + for ($i = 1, $count = count($paths); $i < $count; ++$i) { + if (strpos($paths[$i], end($uniquePaths)) === 0) { + continue; + } + + $uniquePaths[] = $paths[$i]; + } + + return $uniquePaths; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index fd75a66fa0..c35ee55d97 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -1355,16 +1355,28 @@ private function getClassDiscriminatorValues(ClassMetadata $metadata) private function handleCollections($document, $options) { // Collection deletions (deletions of complete collections) + $collections = []; foreach ($this->uow->getScheduledCollections($document) as $coll) { - if ($this->uow->isCollectionScheduledForDeletion($coll)) { - $this->cp->delete($coll, $options); + if (! $this->uow->isCollectionScheduledForDeletion($coll)) { + continue; } + + $collections[] = $coll; + } + if (! empty($collections)) { + $this->cp->deleteAll($document, $collections, $options); } // Collection updates (deleteRows, updateRows, insertRows) + $collections = []; foreach ($this->uow->getScheduledCollections($document) as $coll) { - if ($this->uow->isCollectionScheduledForUpdate($coll)) { - $this->cp->update($coll, $options); + if (! $this->uow->isCollectionScheduledForUpdate($coll)) { + continue; } + + $collections[] = $coll; + } + if (! empty($collections)) { + $this->cp->updateAll($document, $collections, $options); } // Take new snapshots from visited collections foreach ($this->uow->getVisitedCollections($document) as $coll) { diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php index 2f37a39464..824f0a57e1 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php @@ -2,13 +2,32 @@ namespace Doctrine\ODM\MongoDB\Tests\Functional; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ODM\MongoDB\Tests\BaseTest; use Doctrine\ODM\MongoDB\Persisters\CollectionPersister; use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Tests\QueryLogger; class CollectionPersisterTest extends BaseTest { + /** + * @var QueryLogger + */ + private $ql; + + protected function getConfiguration() + { + if ( ! isset($this->ql)) { + $this->ql = new QueryLogger(); + } + + $config = parent::getConfiguration(); + $config->setLoggerCallable($this->ql); + + return $config; + } + public function testDeleteReferenceMany() { $persister = $this->getCollectionPersister(); @@ -54,9 +73,80 @@ public function testDeleteNestedEmbedMany() $this->assertTrue(isset($check['categories'][1]), 'Test that the category with the children still exists'); } - public function testDeleteRows() + public function testDeleteAllEmbedMany() + { + $persister = $this->getCollectionPersister(); + $user = $this->getTestUser('jwage'); + $persister->deleteAll($user, [$user->categories], []); + $user = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertArrayNotHasKey('categories', $user, 'Test that the categories field was deleted'); + } + + public function testDeleteAllReferenceMany() + { + $persister = $this->getCollectionPersister(); + $user = $this->getTestUser('jwage'); + $persister->deleteAll($user, [$user->phonenumbers], []); + $user = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertArrayNotHasKey('phonenumbers', $user, 'Test that the phonenumbers field was deleted'); + } + + public function testDeleteAllNestedEmbedMany() { $persister = $this->getCollectionPersister(); + $user = $this->getTestUser('jwage'); + $this->ql->clear(); + $persister->deleteAll( + $user, + [$user->categories[0]->children[0]->children, $user->categories[0]->children[1]->children], + [] + ); + $this->assertCount(1, $this->ql, 'Deletion of several embedded-many collections of one document requires one query'); + $check = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertFalse(isset($check['categories']['0']['children'][0]['children'])); + $this->assertFalse(isset($check['categories']['0']['children'][1]['children'])); + $this->ql->clear(); + $persister->deleteAll( + $user, + [$user->categories[0]->children, $user->categories[1]->children], + [] + ); + $this->assertCount(1, $this->ql, 'Deletion of several embedded-many collections of one document requires one query'); + $check = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertFalse(isset($check['categories'][0]['children']), 'Test that the nested children categories field was deleted'); + $this->assertTrue(isset($check['categories'][0]), 'Test that the category with the children still exists'); + $this->assertFalse(isset($check['categories'][1]['children']), 'Test that the nested children categories field was deleted'); + $this->assertTrue(isset($check['categories'][1]), 'Test that the category with the children still exists'); + } + + public function testDeleteAllNestedEmbedManyAndNestedParent() + { + $persister = $this->getCollectionPersister(); + $user = $this->getTestUser('jwage'); + $this->ql->clear(); + $persister->deleteAll( + $user, + [$user->categories[0]->children[0]->children, $user->categories[0]->children[1]->children], + [] + ); + $this->assertCount(1, $this->ql, 'Deletion of several embedded-many collections of one document requires one query'); + $check = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertFalse(isset($check['categories']['0']['children'][0]['children'])); + $this->assertFalse(isset($check['categories']['0']['children'][1]['children'])); + $this->ql->clear(); + $persister->deleteAll( + $user, + [$user->categories[0]->children, $user->categories[0]->children[1]->children, $user->categories], + [] + ); + $this->assertCount(1, $this->ql, 'Deletion of several embedded-many collections of one document requires one query'); + $check = $this->dm->getDocumentCollection(CollectionPersisterUser::class)->findOne(['username' => 'jwage']); + $this->assertFalse(isset($check['categories']), 'Test that the nested categories field was deleted'); + } + + + public function testDeleteRows() + { $user = $this->getTestUser('jwage'); unset($user->phonenumbers[0]); @@ -68,7 +158,9 @@ public function testDeleteRows() unset($user->categories[0]->children[1]->children[0]); unset($user->categories[0]->children[1]->children[1]); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount(2, $this->ql, 'Modification of several embedded-many collections of one document requires two queries'); $check = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterUser')->findOne(array('username' => 'jwage')); @@ -83,7 +175,9 @@ public function testDeleteRows() unset($user->categories[0]); unset($user->categories[1]); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount(2, $this->ql, 'Modification of embedded-many collection of one document requires two queries'); $check = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterUser')->findOne(array('username' => 'jwage')); $this->assertFalse(isset($check['categories'][0])); @@ -95,7 +189,9 @@ public function testInsertRows() $user = $this->getTestUser('jwage'); $user->phonenumbers[] = new CollectionPersisterPhonenumber('6155139185'); $user->phonenumbers[] = new CollectionPersisterPhonenumber('6155139185'); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount(2, $this->ql, 'Modification of embedded-many collection of one document requires two queries'); $check = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterUser')->findOne(array('username' => 'jwage')); $this->assertCount(4, $check['phonenumbers']); @@ -104,7 +200,13 @@ public function testInsertRows() $user->categories[] = new CollectionPersisterCategory('Test'); $user->categories[] = new CollectionPersisterCategory('Test'); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collection of one document requires one query since no existing collection elements was removed' + ); $check = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterUser')->findOne(array('username' => 'jwage')); $this->assertCount(4, $check['categories']); @@ -113,7 +215,13 @@ public function testInsertRows() $user->categories[3]->children[1] = new CollectionPersisterCategory('Test'); $user->categories[3]->children[1]->children[0] = new CollectionPersisterCategory('Test'); $user->categories[3]->children[1]->children[1] = new CollectionPersisterCategory('Test'); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collection of one document requires one query since no existing collection elements was removed' + ); $check = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterUser')->findOne(array('username' => 'jwage')); $this->assertCount(2, $check['categories'][3]['children']); @@ -166,7 +274,13 @@ public function testNestedEmbedManySetStrategy() $commentA->comments->set('b', $commentAB); $this->dm->persist($post); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collection of one document requires no additional queries' + ); $doc = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterPost')->findOne(array('post' => 'postA')); @@ -187,7 +301,13 @@ public function testNestedEmbedManySetStrategy() $commentB->comments->set('a', $commentBA); $this->dm->persist($post); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collection of one document requires one query since no existing collection elements was removed' + ); $doc = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterPost')->findOne(array('post' => 'postA')); @@ -207,7 +327,13 @@ public function testNestedEmbedManySetStrategy() $commentAB->comment = 'commentAB-modified'; $this->dm->persist($post); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collection of one document requires one query since no existing collection elements was removed' + ); $doc = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterPost')->findOne(array('post' => 'postA')); @@ -224,7 +350,13 @@ public function testNestedEmbedManySetStrategy() unset($post->comments['b']); $this->dm->persist($post); + $this->ql->clear(); $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collection of one document requires one query since collection, from which element was removed, have "set" store strategy.' + ); $doc = $this->dm->getDocumentCollection(__NAMESPACE__ . '\CollectionPersisterPost')->findOne(array('post' => 'postA')); @@ -251,6 +383,204 @@ public function testFindBySetStrategyKey() $this->assertSame($post, $this->dm->getRepository(get_class($post))->findOneBy(array('comments.a.by' => 'userA'))); $this->assertSame($post, $this->dm->getRepository(get_class($post))->findOneBy(array('comments.a.comments.b.by' => 'userB'))); } + + public function testPersistSeveralNestedEmbedManySetStrategy() + { + $structure = new CollectionPersisterStructure(); + $structure->set->add(new CollectionPersisterNestedStructure('nested1')); + $structure->set->add(new CollectionPersisterNestedStructure('nested2')); + $structure->set2->add(new CollectionPersisterNestedStructure('nested3')); + $structure->set2->add(new CollectionPersisterNestedStructure('nested4')); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collections of one document by "set" strategy requires no additional queries' + ); + + $structure->set->clear(); + $structure->set->add(new CollectionPersisterNestedStructure('nested5')); + $structure->set2->add(new CollectionPersisterNestedStructure('nested6')); + + $this->ql->clear(); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collections of one document by "set" strategy requires one query' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } + + public function testPersistSeveralNestedEmbedManySetArrayStrategy() + { + $structure = new CollectionPersisterStructure(); + $structure->setArray->add(new CollectionPersisterNestedStructure('nested1')); + $structure->setArray->add(new CollectionPersisterNestedStructure('nested2')); + $structure->setArray2->add(new CollectionPersisterNestedStructure('nested3')); + $structure->setArray2->add(new CollectionPersisterNestedStructure('nested4')); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collections of one document by "setArray" strategy requires no additional queries' + ); + + $structure->setArray->clear(); + $structure->setArray->add(new CollectionPersisterNestedStructure('nested5')); + $structure->setArray2->add(new CollectionPersisterNestedStructure('nested6')); + + $this->ql->clear(); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Modification of embedded-many collections of one document by "setArray" strategy requires one query' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } + + public function testPersistSeveralNestedEmbedManyAddToSetStrategy() + { + $structure = new CollectionPersisterStructure(); + $structure->addToSet->add(new CollectionPersisterNestedStructure('nested1')); + $structure->addToSet->add(new CollectionPersisterNestedStructure('nested2')); + $structure->addToSet2->add(new CollectionPersisterNestedStructure('nested3')); + $structure->addToSet2->add(new CollectionPersisterNestedStructure('nested4')); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 2, + $this->ql, + 'Insertion of embedded-many collections of one document by "addToSet" strategy requires one additional query' + ); + + $structure->addToSet->clear(); + $structure->addToSet->add(new CollectionPersisterNestedStructure('nested5')); + $structure->addToSet2->add(new CollectionPersisterNestedStructure('nested6')); + + $this->ql->clear(); + $this->dm->flush(); + $this->assertCount( + 2, + $this->ql, + 'Modification of embedded-many collections of one document by "addToSet" strategy requires two queries' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } + + public function testPersistSeveralNestedEmbedManyPushAllStrategy() + { + $structure = new CollectionPersisterStructure(); + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested1')); + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested2')); + $structure->pushAll2->add(new CollectionPersisterNestedStructure('nested3')); + $structure->pushAll2->add(new CollectionPersisterNestedStructure('nested4')); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collections of one document by "pushAll" strategy requires no additional queries' + ); + + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested5')); + $structure->pushAll2->add(new CollectionPersisterNestedStructure('nested6')); + + $this->ql->clear(); + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 2, + $this->ql, + 'Modification of embedded-many collections of one document by "pushAll" strategy requires two queries' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } + + public function testPersistSeveralNestedEmbedManyDifferentStrategies() + { + $structure = new CollectionPersisterStructure(); + $structure->set->add(new CollectionPersisterNestedStructure('nested1')); + $structure->setArray->add(new CollectionPersisterNestedStructure('nested2')); + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested3')); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collections of one document by "set", "setArray" and "pushAll" strategies requires no additional queries' + ); + + $structure->set->remove(0); + $structure->set->add(new CollectionPersisterNestedStructure('nested5')); + $structure->pushAll->remove(0); + $structure->setArray->add(new CollectionPersisterNestedStructure('nested6')); + $structure->setArray->remove(0); + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested7')); + + $this->ql->clear(); + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 4, + $this->ql, + 'Modification of embedded-many collections of one document by "set", "setArray" and "pushAll" strategies requires two queries' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } + + public function testPersistSeveralNestedEmbedManyDifferentStrategiesDeepNesting() + { + $structure = new CollectionPersisterStructure(); + $nested1 = new CollectionPersisterNestedStructure('nested1'); + $nested2 = new CollectionPersisterNestedStructure('nested2'); + $nested3 = new CollectionPersisterNestedStructure('nested3'); + $nested1->setArray->add(new CollectionPersisterNestedStructure('setArray_nested1')); + $nested2->pushAll->add(new CollectionPersisterNestedStructure('pushAll_nested1')); + $nested3->set->add(new CollectionPersisterNestedStructure('set_nested1')); + $structure->set->add($nested1); + $structure->setArray->add($nested2); + $structure->pushAll->add($nested3); + + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 1, + $this->ql, + 'Insertion of embedded-many collections of one document by "set", "setArray" and "pushAll" strategies requires no additional queries' + ); + + $structure->set->remove(0); + $structure->set->add(new CollectionPersisterNestedStructure('nested5')); + $structure->setArray->get(0)->set->add(new CollectionPersisterNestedStructure('set_nested1')); + $structure->pushAll->get(0)->set->clear(); + $structure->pushAll->add(new CollectionPersisterNestedStructure('nested5')); + $structure->pushAll->remove(0); + + $this->ql->clear(); + $this->dm->persist($structure); + $this->dm->flush(); + $this->assertCount( + 5, + $this->ql, + 'Modification of embedded-many collections of one document by "set", "setArray" and "pushAll" strategies requires two queries' + ); + + $this->assertSame($structure, $this->dm->getRepository(get_class($structure))->findOneBy(['id' => $structure->id])); + } } /** @ODM\Document(collection="user_collection_persister_test") */ @@ -313,7 +643,7 @@ class CollectionPersisterPost function __construct($post) { - $this->comments = new \Doctrine\Common\Collections\ArrayCollection(); + $this->comments = new ArrayCollection(); $this->post = $post; } @@ -337,8 +667,98 @@ class CollectionPersisterComment function __construct($comment, $by) { - $this->comments = new \Doctrine\Common\Collections\ArrayCollection(); + $this->comments = new ArrayCollection(); $this->comment = $comment; $this->by = $by; } } + +/** @ODM\Document(collection="structure_collection_persister_test") */ +class CollectionPersisterStructure +{ + /** @ODM\Id */ + public $id; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="addToSet") */ + public $addToSet; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="addToSet") */ + public $addToSet2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="set") */ + public $set; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="set") */ + public $set2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="setArray") */ + public $setArray; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="setArray") */ + public $setArray2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="pushAll") */ + public $pushAll; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="pushAll") */ + public $pushAll2; + + public function __construct() + { + $this->addToSet = new ArrayCollection(); + $this->addToSet2 = new ArrayCollection(); + $this->set = new ArrayCollection(); + $this->set2 = new ArrayCollection(); + $this->setArray = new ArrayCollection(); + $this->setArray2 = new ArrayCollection(); + $this->pushAll = new ArrayCollection(); + $this->pushAll2 = new ArrayCollection(); + } +} + +/** @ODM\EmbeddedDocument */ +class CollectionPersisterNestedStructure +{ + /** @ODM\Id */ + public $id; + + /** @ODM\Field(type="string") */ + public $field; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="addToSet") */ + public $addToSet; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="addToSet") */ + public $addToSet2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="set") */ + public $set; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="set") */ + public $set2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="setArray") */ + public $setArray; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="setArray") */ + public $setArray2; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="pushAll") */ + public $pushAll; + + /** @ODM\EmbedMany(targetDocument=CollectionPersisterNestedStructure::class, strategy="pushAll") */ + public $pushAll2; + + public function __construct($field) + { + $this->field = $field; + $this->addToSet = new ArrayCollection(); + $this->addToSet2 = new ArrayCollection(); + $this->set = new ArrayCollection(); + $this->set2 = new ArrayCollection(); + $this->setArray = new ArrayCollection(); + $this->setArray2 = new ArrayCollection(); + $this->pushAll = new ArrayCollection(); + $this->pushAll2 = new ArrayCollection(); + } +}