From d90bba97225c1d3ff76931fd43c4309e5daaa17b Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 11 Jun 2012 14:01:57 +0200 Subject: [PATCH 1/6] [Sharding] added new "QueryFields" annotation --- .../Annotations/DoctrineAnnotations.php | 2 + .../ODM/MongoDB/Mapping/ClassMetadata.php | 4 ++ .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 39 +++++++++++++++++++ .../Mapping/Driver/AnnotationDriver.php | 2 + .../ODM/MongoDB/Mapping/Driver/XmlDriver.php | 12 ++++++ .../ODM/MongoDB/Mapping/Driver/YamlDriver.php | 3 ++ .../Mapping/AbstractMappingDriverTest.php | 16 +++++++- .../Tests/Mapping/ClassMetadataTest.php | 4 +- ...ine.ODM.MongoDB.Tests.Mapping.User.dcm.xml | 6 ++- ...ine.ODM.MongoDB.Tests.Mapping.User.dcm.yml | 3 +- 10 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php index 1e9c34ccb3..8d6367a54e 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php @@ -61,6 +61,8 @@ final class DiscriminatorField extends Annotation final class DiscriminatorMap extends Annotation {} /** @Annotation */ final class DiscriminatorValue extends Annotation {} +/** @Annotation */ +final class QueryFields extends Annotation {} /** @Annotation */ final class Indexes extends Annotation {} diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index a7ccee1d61..6efd926f64 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -157,6 +157,10 @@ public function __sleep() $serialized[] = 'file'; } + if($this->hasQueryFields()) { + $serialized[] = 'queryFields'; + } + return $serialized; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index 4605a0ed2e..0e5dddb186 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -182,6 +182,11 @@ class ClassMetadataInfo implements \Doctrine\Common\Persistence\Mapping\ClassMet */ public $requireIndexes = false; + /** + * READ-ONLY: The fields for the query for the document collection. + */ + public $queryFields = array(); + /** * READ-ONLY: The name of the document class. */ @@ -681,6 +686,40 @@ public function hasIndexes() return $this->indexes ? true : false; } + /** + * Returns the custom query fields for this Document. + * + * @return array $fields The fields for the query. + */ + public function getQueryFields() + { + return $this->queryFields; + } + + /** + * Checks whether this document has custom query fields or not. + * + * @return boolean + */ + public function hasQueryFields() + { + return $this->queryFields ? true : false; + } + + /** + * Sets the custom query fields for this Document. + * + * @param array $fields Array of fields for the query. + */ + public function setQueryFields($fields) + { + if(!is_array($fields)) { + $fields = array($fields); + } + + $this->queryFields = $fields; + } + /** * Sets the change tracking policy used by this class. * diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php index 4026b88f28..5161eda133 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php @@ -152,6 +152,8 @@ public function loadMetadataForClass($className, ClassMetadataInfo $class) foreach (is_array($annot->value) ? $annot->value : array($annot->value) as $index) { $this->addIndex($class, $index); } + } elseif ($annot instanceof ODM\QueryFields) { + $class->setQueryFields($annot->value); } elseif ($annot instanceof ODM\InheritanceType) { $class->setInheritanceType(constant('Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata::INHERITANCE_TYPE_'.$annot->value)); } elseif ($annot instanceof ODM\DiscriminatorField) { diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php index dcf82496d7..5c6b28c9d4 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php @@ -87,6 +87,9 @@ public function loadMetadataForClass($className, ClassMetadataInfo $class) } $class->setDiscriminatorMap($map); } + if (isset($xmlRoot->{'query-fields'})) { + $this->setQueryFields($class, $xmlRoot->{'query-fields'}); + } if (isset($xmlRoot->{'indexes'})) { foreach($xmlRoot->{'indexes'}->{'index'} as $index) { $this->addIndex($class, $index); @@ -301,6 +304,15 @@ private function addIndex(ClassMetadataInfo $class, SimpleXmlElement $xmlIndex) $class->addIndex($index['keys'], $index['options']); } + private function setQueryFields(ClassMetadataInfo $class, SimpleXmlElement $xmlIndex) + { + $fields = array(); + foreach ($xmlIndex->{'key'} as $key) { + $fields[(string) $key['name']] = (string) $key['name']; + } + $class->setQueryFields(array_values($fields)); + } + protected function loadMappingFile($file) { $result = array(); diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php index 37d6396733..4648df5d50 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php @@ -65,6 +65,9 @@ public function loadMetadataForClass($className, ClassMetadataInfo $class) } elseif ($element['type'] === 'embeddedDocument') { $class->isEmbeddedDocument = true; } + if (isset($element['queryFields'])) { + $class->setQueryFields($element['queryFields']); + } if (isset($element['indexes'])) { foreach($element['indexes'] as $index) { $class->addIndex($index['keys'], isset($index['options']) ? $index['options'] : array()); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php index 6c6f6aa629..e816e0829c 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php @@ -217,12 +217,25 @@ public function testIndexes($class) return $class; } + + /** + * @depends testCustomFieldName + * @param ClassMetadata $class + */ + public function testQueryFields($class) + { + $this->assertTrue(isset($class->queryFields)); + $this->assertEquals(array('id', 'name'), $class->queryFields); + + return $class; + } } /** * @ODM\Document(collection="cms_users") * @ODM\DiscriminatorField(fieldName="discr") * @ODM\DiscriminatorMap({"default"="Doctrine\ODM\MongoDB\Tests\Mapping\User"}) + * @ODM\QueryFields({"id", "name"}) */ class User { @@ -366,5 +379,6 @@ public static function loadMetadata(ClassMetadata $metadata) $metadata->addIndex(array('username' => 'desc'), array('unique' => true)); $metadata->addIndex(array('email' => 'desc'), array('unique' => true, 'dropDups' => true)); $metadata->addIndex(array('mysqlProfileId' => 'desc'), array('unique' => true, 'dropDups' => true)); + $metadata->setQueryFields(array('id', 'name')); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php index b1e2dc7782..975c9eb1a4 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php @@ -29,6 +29,7 @@ public function testClassMetadataInstanceSerialization() $cm->setDiscriminatorField(array('name' => 'disc')); $cm->mapOneEmbedded(array('fieldName' => 'phonenumbers', 'targetDocument' => 'Bar')); $cm->setFile('customFileProperty'); + $cm->setQueryFields(array('name')); $this->assertTrue(is_array($cm->getFieldMapping('phonenumbers'))); $this->assertEquals(1, count($cm->fieldMappings)); @@ -48,6 +49,7 @@ public function testClassMetadataInstanceSerialization() $this->assertTrue(is_array($cm->getFieldMapping('phonenumbers'))); $this->assertEquals(1, count($cm->fieldMappings)); $this->assertEquals('customFileProperty', $cm->file); + $this->assertEquals(array('name'), $cm->queryFields); $mapping = $cm->getFieldMapping('phonenumbers'); $this->assertEquals('Documents\Bar', $mapping['targetDocument']); } @@ -182,4 +184,4 @@ public function testDuplicateFieldAndAssocationMapping2_ThrowsException() $this->setExpectedException('Doctrine\ODM\MongoDB\MongoDBException'); $cm->mapField(array('fieldName' => 'name', 'columnName' => 'name')); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.xml b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.xml index d0a9fdb6af..2f9c4c8d05 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.xml +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.xml @@ -10,6 +10,10 @@ + + + + @@ -53,4 +57,4 @@ - \ No newline at end of file + diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.yml b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.yml index 47bac673a6..b8839e841a 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.yml +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.User.dcm.yml @@ -5,6 +5,7 @@ Doctrine\ODM\MongoDB\Tests\Mapping\User: fieldName: discr discriminatorMap: default: Doctrine\ODM\MongoDB\Tests\Mapping\User + queryFields: [id, name] fields: id: type: id @@ -56,4 +57,4 @@ Doctrine\ODM\MongoDB\Tests\Mapping\User: lifecycleCallbacks: prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ] - postPersist: [ doStuffOnPostPersist ] \ No newline at end of file + postPersist: [ doStuffOnPostPersist ] From b6e2885e10f71e564be551357d7548f642b50b5e Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 11 Jun 2012 14:04:58 +0200 Subject: [PATCH 2/6] [Sharding] updated DocumentPersister to use custom query fields in updates and upserts. Removed also the id from the refresh method --- .../MongoDB/Persisters/DocumentPersister.php | 73 +++++++++++++------ lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 3 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index af27317daa..444a497cb4 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -240,13 +240,17 @@ public function executeInserts(array $options = array()) $upsertOptions = $options; $upsertOptions['upsert'] = true; foreach ($upserts as $oid => $data) { - $criteria = array('_id' => $data[$this->cmd.'set']['_id']); - unset($data[$this->cmd.'set']['_id']); + $query = $this->getDocumentQuery($this->queuedInserts[$oid]); + foreach($query as $field => $value) { + if(isset($data[$this->cmd.'set'][$field])) { + unset($data[$this->cmd.'set'][$field]); + } + } // stupid php if (empty($data[$this->cmd.'set'])) { $data[$this->cmd.'set'] = new \stdClass; } - $this->collection->update($criteria, $data, $upsertOptions); + $this->collection->update($query, $data, $upsertOptions); } } @@ -263,13 +267,10 @@ public function executeInserts(array $options = array()) */ public function update($document, array $options = array()) { - $id = $this->uow->getDocumentIdentifier($document); $update = $this->pb->prepareUpdateData($document); if ( ! empty($update)) { - - $id = $this->class->getDatabaseIdentifierValue($id); - $query = array('_id' => $id); + $query = $this->getDocumentQuery($document); // Include versioning logic to set the new version value in the database // and to ensure the version has not changed since this document object instance @@ -320,8 +321,7 @@ public function update($document, array $options = array()) */ public function delete($document, array $options = array()) { - $id = $this->uow->getDocumentIdentifier($document); - $query = array('_id' => $this->class->getDatabaseIdentifierValue($id)); + $query = $this->getDocumentQuery($document); if ($this->class->isLockable) { $query[$this->class->lockField] = array($this->cmd . 'exists' => false); @@ -338,13 +338,12 @@ public function delete($document, array $options = array()) /** * Refreshes a managed document. * - * @param array $id The identifier of the document. * @param object $document The document to refresh. */ - public function refresh($id, $document) + public function refresh($document) { - $class = $this->dm->getClassMetadata(get_class($document)); - $data = $this->collection->findOne(array('_id' => $id)); + $query = $this->getDocumentQuery($document); + $data = $this->collection->findOne($query); $data = $this->hydratorFactory->hydrate($document, $data); $this->uow->setOriginalDocumentData($document, $data); } @@ -449,8 +448,8 @@ private function wrapCursor(BaseCursor $cursor) */ public function exists($document) { - $id = $this->class->getIdentifierObject($document); - return (bool) $this->collection->findOne(array(array('_id' => $id)), array('_id')); + $query = $this->getDocumentQuery($document); + return (bool) $this->collection->findOne($query, array('_id')); } /** @@ -461,10 +460,9 @@ public function exists($document) */ public function lock($document, $lockMode) { - $id = $this->uow->getDocumentIdentifier($document); - $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id)); + $query = $this->getDocumentQuery($document); $lockMapping = $this->class->fieldMappings[$this->class->lockField]; - $this->collection->update($criteria, array($this->cmd.'set' => array($lockMapping['name'] => $lockMode))); + $this->collection->update($query, array($this->cmd.'set' => array($lockMapping['name'] => $lockMode))); $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode); } @@ -475,10 +473,9 @@ public function lock($document, $lockMode) */ public function unlock($document) { - $id = $this->uow->getDocumentIdentifier($document); - $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id)); + $query = $this->getDocumentQuery($document); $lockMapping = $this->class->fieldMappings[$this->class->lockField]; - $this->collection->update($criteria, array($this->cmd.'unset' => array($lockMapping['name'] => true))); + $this->collection->update($query, array($this->cmd.'unset' => array($lockMapping['name'] => true))); $this->class->reflFields[$this->class->lockField]->setValue($document, null); } @@ -740,6 +737,40 @@ private function loadReferenceManyWithRepositoryMethod(PersistentCollection $col } } + /** + * Gets the query for the document. + * + * @param mixed $document + * + * @return array $query + */ + public function getDocumentQuery($document) + { + if($this->class->hasQueryFields()) { + $changeSet = $this->uow->getDocumentChangeSet($document); + $query = array(); + foreach($this->class->queryFields as $field) { + if(isset($changeSet[$field])) { + if(null !== $changeSet[$field][0]) { + $query[$field] = $changeSet[$field][0]; + } else { + // upsert new document + $query[$field] = $changeSet[$field][1]; + } + } else { + $query[$field] = $this->class->getFieldValue($document, $field); + } + } + + $query = $this->prepareQuery($query); + } else { + $id = $this->uow->getDocumentIdentifier($document); + $query = array('_id' => $this->class->getDatabaseIdentifierValue($id)); + } + + return $query; + } + /** * Prepares a sort array and converts PHP property names to MongoDB field names. * diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 571f6153a0..91a8bc2f75 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -2100,8 +2100,7 @@ private function doRefresh($document, array &$visited) $class = $this->dm->getClassMetadata(get_class($document)); if ($this->getDocumentState($document) == self::STATE_MANAGED) { - $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]); - $this->getDocumentPersister($class->name)->refresh($id, $document); + $this->getDocumentPersister($class->name)->refresh($document); } else { throw new \InvalidArgumentException("Document is not MANAGED."); } From 439838239e5f2aa06c9e680ee93f046cb8cdcc3b Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 11 Jun 2012 14:06:36 +0200 Subject: [PATCH 3/6] fixed getIdentifierValue to return not only strings --- lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index 0e5dddb186..547cd140eb 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -1328,7 +1328,7 @@ public function getIdentifierValue($document) if ($document instanceof Proxy && !$document->__isInitialized()) { return $document->__identifier__; } - return (string) $this->reflFields[$this->identifier]->getValue($document); + return $this->reflFields[$this->identifier]->getValue($document); } /** From 76b35284ad27d50d04745c3476fffdf8a377a24c Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 11 Jun 2012 14:07:41 +0200 Subject: [PATCH 4/6] [Sharding] use custom query fields for test user --- tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php | 8 ++++++-- tests/Documents/User.php | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php index 78dd26fd71..e782c1949e 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php @@ -149,8 +149,12 @@ public function testUpdateQuery() $query = $qb->getQuery(); $result = $query->execute(); - $this->dm->refresh($this->user); - $this->assertEquals('crap', $this->user->getUsername()); + $qb = $this->dm->createQueryBuilder('Documents\User') + ->find() + ->field('username')->equals('crap'); + $query = $qb->getQuery(); + $user = $query->getSingleResult(); + $this->assertNotNull($user); } public function testUpsertUpdateQuery() diff --git a/tests/Documents/User.php b/tests/Documents/User.php index 1291615180..e42311e43b 100644 --- a/tests/Documents/User.php +++ b/tests/Documents/User.php @@ -8,6 +8,7 @@ /** * @ODM\Document(collection="users") * @ODM\InheritanceType("COLLECTION_PER_CLASS") + * @ODM\QueryFields({"id", "username"}) */ class User extends BaseDocument { From 373cecc63ceadd4c782c16225494e8ef9d8b1490 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 11 Jun 2012 16:34:52 +0200 Subject: [PATCH 5/6] fixed cs --- .../ODM/MongoDB/Persisters/DocumentPersister.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index 444a497cb4..29a015325c 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -241,8 +241,8 @@ public function executeInserts(array $options = array()) $upsertOptions['upsert'] = true; foreach ($upserts as $oid => $data) { $query = $this->getDocumentQuery($this->queuedInserts[$oid]); - foreach($query as $field => $value) { - if(isset($data[$this->cmd.'set'][$field])) { + foreach ($query as $field => $value) { + if (isset($data[$this->cmd.'set'][$field])) { unset($data[$this->cmd.'set'][$field]); } } @@ -746,12 +746,12 @@ private function loadReferenceManyWithRepositoryMethod(PersistentCollection $col */ public function getDocumentQuery($document) { - if($this->class->hasQueryFields()) { + if ($this->class->hasQueryFields()) { $changeSet = $this->uow->getDocumentChangeSet($document); $query = array(); - foreach($this->class->queryFields as $field) { - if(isset($changeSet[$field])) { - if(null !== $changeSet[$field][0]) { + foreach ($this->class->queryFields as $field) { + if (isset($changeSet[$field])) { + if (null !== $changeSet[$field][0]) { $query[$field] = $changeSet[$field][0]; } else { // upsert new document From 9257840038ee23059d6dd65acf1bc530b905d342 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Fri, 15 Jun 2012 12:18:36 +0200 Subject: [PATCH 6/6] [Sharding] added initial DBRef logic --- lib/Doctrine/ODM/MongoDB/DocumentManager.php | 86 +++-- .../ODM/MongoDB/DocumentNotFoundException.php | 4 +- .../ODM/MongoDB/Hydrator/HydratorFactory.php | 28 +- .../ODM/MongoDB/Mapping/ClassMetadataInfo.php | 44 ++- .../MongoDB/Persisters/DocumentPersister.php | 129 ++++--- .../ODM/MongoDB/Proxy/ProxyFactory.php | 28 +- lib/Doctrine/ODM/MongoDB/Query/Expr.php | 35 +- lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 11 +- .../MongoDB/Tests/Functional/ShardingTest.php | 316 ++++++++++++++++++ .../ODM/MongoDB/Tests/Query/ExprTest.php | 4 +- 10 files changed, 550 insertions(+), 135 deletions(-) create mode 100644 tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardingTest.php diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index 33921f42d5..90e9b13a92 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -534,17 +534,20 @@ public function flush($document = null, array $options = array()) * * @param string $documentName * @param string|object $identifier + * @param mixed $query + * @param array $sort * @return mixed|object The document reference. */ - public function getReference($documentName, $identifier) + public function getReference($documentName, $identifier, $query = null, array $sort = null) { $class = $this->metadataFactory->getMetadataFor($documentName); // Check identity map first, if its already in there just return it. - if ($document = $this->unitOfWork->tryGetById($identifier, $class->rootDocumentName)) { + if ($identifier && $document = $this->unitOfWork->tryGetById($identifier, $class->rootDocumentName)) { return $document; } - $document = $this->proxyFactory->getProxy($class->name, $identifier); + + $document = $this->proxyFactory->getProxy($class->name, $identifier, $query ?: $identifier, $sort); $this->unitOfWork->registerManaged($document, $identifier, array()); return $document; @@ -677,42 +680,87 @@ public function getClassNameFromDiscriminatorValue(array $mapping, $value) * * @param mixed $document A document object * @param array $referenceMapping Mapping for the field the references the document + * @param boolean $forQuery Is the DBRef for a query? * * @return array A DBRef array */ - public function createDBRef($document, array $referenceMapping = null) + public function createDBRef($document, array $referenceMapping = null, $forQuery = false) { if (!is_object($document)) { throw new \InvalidArgumentException('Cannot create a DBRef, the document is not an object'); } $className = get_class($document); $class = $this->getClassMetadata($className); - $id = $this->unitOfWork->getDocumentIdentifier($document); + $id = $class->getDatabaseIdentifierValue($this->unitOfWork->getDocumentIdentifier($document)); - if (isset($referenceMapping['simple']) && $referenceMapping['simple']) { - return $class->getDatabaseIdentifierValue($id); + if ($referenceMapping && isset($referenceMapping['simple']) && $referenceMapping['simple']) { + return $id; } - $dbRef = array( - $this->cmd . 'ref' => $class->getCollection(), - $this->cmd . 'id' => $class->getDatabaseIdentifierValue($id), - $this->cmd . 'db' => $this->getDocumentDatabase($className)->getName() - ); + $dbRef = array(); + if (!$forQuery || null === $referenceMapping || !isset($referenceMapping['targetDocument'])) { + $dbRef[$this->cmd . 'ref'] = $class->getCollection(); + $dbRef[$this->cmd . 'db'] = $this->getDocumentDatabase($className)->getName(); + } - if ($class->discriminatorField) { - $dbRef[$class->discriminatorField['name']] = $class->discriminatorValue; + if ($class->hasQueryFields()) { + foreach ($class->queryFields as $field) { + $dbRef[$this->cmd . ($class->isIdentifier($field) ? 'id' : $class->getDatabaseFieldName($field))] = $class->getDatabaseFieldValue($class->getFieldValue($document, $field), $field); + } + + if (!$forQuery) { + $dbRef[$this->cmd . 'id'] = $id; + } + } else { + $dbRef[$this->cmd . 'id'] = $id; } - // add a discriminator value if the referenced document is not mapped explicitely to a targetDocument - if ($referenceMapping && ! isset($referenceMapping['targetDocument'])) { - $discriminatorField = isset($referenceMapping['discriminatorField']) ? $referenceMapping['discriminatorField'] : '_doctrine_class_name'; - $discriminatorValue = isset($referenceMapping['discriminatorMap']) ? array_search($class->getName(), $referenceMapping['discriminatorMap']) : $class->getName(); - $dbRef[$discriminatorField] = $discriminatorValue; + if (!$forQuery) { + if ($class->discriminatorField) { + $dbRef[$class->discriminatorField['name']] = $class->discriminatorValue; + } + + // add a discriminator value if the referenced document is not mapped explicitely to a targetDocument + if ($referenceMapping && ! isset($referenceMapping['targetDocument'])) { + $discriminatorField = isset($referenceMapping['discriminatorField']) ? $referenceMapping['discriminatorField'] : '_doctrine_class_name'; + $discriminatorValue = isset($referenceMapping['discriminatorMap']) ? array_search($class->getName(), $referenceMapping['discriminatorMap']) : $class->getName(); + $dbRef[$discriminatorField] = $discriminatorValue; + } } return $dbRef; } + /** + * Returns a reference query array from a db ref. + * + * @param mixed $reference + * @param string|object $identifier + * @param ClassMetadata $metadata + * @param array $referenceMapping + * + * @return array A DBRef query array + */ + public function getReferenceQueryFromDBRef($reference, $identifier, ClassMetadata $metadata, array $referenceMapping) + { + if (is_scalar($reference) || !$metadata->hasQueryFields() || isset($referenceMapping['simple']) && $referenceMapping['simple']) { + $query = $identifier; + } else { + $query = array(); + foreach ($metadata->queryFields as $field) { + $fieldName = $metadata->isIdentifier($field) ? 'id' : $metadata->getDatabaseFieldName($field); + if (isset($reference[$this->cmd . $fieldName])) { + $query[$fieldName] = $metadata->getPHPFieldValue($reference[$this->cmd . $fieldName], $field); + } + } + if (empty($query)) { + $query = $identifier; + } + } + + return $query; + } + /** * Throws an exception if the DocumentManager is closed or currently not active. * diff --git a/lib/Doctrine/ODM/MongoDB/DocumentNotFoundException.php b/lib/Doctrine/ODM/MongoDB/DocumentNotFoundException.php index 5428d5627c..fd695b5bed 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentNotFoundException.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentNotFoundException.php @@ -30,8 +30,8 @@ */ class DocumentNotFoundException extends MongoDBException { - public static function documentNotFound($className, $identifier) + public static function documentNotFound($className, $query) { - return new self(sprintf('The "%s" document with identifier "%s" could not be found.', $className, $identifier)); + return new self(sprintf('The "%s" document with query "%s" could not be found.', $className, json_encode($query))); } } diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php index 724dc79d0c..8f215b0925 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php @@ -238,7 +238,8 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, } \$targetMetadata = \$this->dm->getClassMetadata(\$className); \$id = \$targetMetadata->getPHPIdentifierValue(\$mongoId); - \$return = \$this->dm->getReference(\$className, \$id); + \$query = \$this->dm->getReferenceQueryFromDBRef(\$reference, \$id, \$targetMetadata, \$this->class->fieldMappings['%2\$s']); + \$return = \$this->dm->getReference(\$className, \$id, \$query); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; } @@ -252,6 +253,7 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) { $code .= sprintf(<<class->fieldMappings['%2\$s']['targetDocument']; \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); @@ -264,19 +266,17 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $mapping['repositoryMethod'] ); } else { + // TODO: Sharding $code .= sprintf(<<class->fieldMappings['%2\$s']; - \$className = \$mapping['targetDocument']; - \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']); - \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']]; - \$mappedByFieldName = isset(\$mappedByMapping['simple']) && \$mappedByMapping['simple'] ? \$mapping['mappedBy'] : \$mapping['mappedBy'].'.id'; - \$criteria = array_merge( - array(\$mappedByFieldName => \$data['_id']), - isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array() - ); - \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array(); - \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort); + \$qb = \$this->dm->createQueryBuilder(\$mapping['targetDocument']); + if (isset(\$mapping['criteria'])) { + \$qb->setQueryArray(\$mapping['criteria']); + } + \$qb->field(\$mapping['mappedBy'])->references(\$document); + \$return = \$this->dm->getReference(\$mapping['targetDocument'], null, \$qb->getQueryArray(), isset(\$mapping['sort']) ? \$mapping['sort'] : null); \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); \$hydratedData['%2\$s'] = \$return; @@ -287,9 +287,10 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, ); } } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) { + $comment = $mapping['association'] === ClassMetadata::REFERENCE_MANY ? 'ReferenceMany' : 'EmbedMany'; $code .= sprintf(<<dm, \$this->unitOfWork, '$'); \$return->setHints(\$hints); @@ -304,7 +305,8 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, EOF , $mapping['name'], - $mapping['fieldName'] + $mapping['fieldName'], + $comment ); } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) { $code .= sprintf(<<queryFields ? true : false; + return (boolean) $this->queryFields; } /** @@ -713,8 +713,11 @@ public function hasQueryFields() */ public function setQueryFields($fields) { - if(!is_array($fields)) { - $fields = array($fields); + $fields = is_array($fields) ? $fields : array($fields); + foreach ($fields as $field) { + if (strpos($field, '.') !== false) { + return new MongoDBException(sprintf('Embedded or reference shard keys not supported. Key "%s" in class "%s"', $field, $this->name)); + } } $this->queryFields = $fields; @@ -1162,6 +1165,17 @@ public function addInheritedFieldMapping(array $fieldMapping) $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping; } + /** + * Gets the database field name. + * + * @param string $fieldName + * @return string + */ + public function getDatabaseFieldName($fieldName) + { + return $this->fieldMappings[$fieldName]['name']; + } + /** * Checks whether the class has a mapped association with the given field name. * @@ -1385,6 +1399,30 @@ public function getFieldValue($document, $field) return $this->reflFields[$field]->getValue($document); } + /** + * Casts the field value to its php type. + * + * @param mixed $value + * @param string $field + */ + public function getPHPFieldValue($value, $field) + { + $type = $this->fieldMappings[$field]['type']; + return Types\Type::getType($type)->convertToPHPValue($value); + } + + /** + * Casts the field value to its database type. + * + * @param mixed $value + * @param string $field + */ + public function getDatabaseFieldValue($value, $field) + { + $type = $this->fieldMappings[$field]['type']; + return Types\Type::getType($type)->convertToDatabaseValue($value); + } + /** * Gets the mapping of a field. * diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index 29a015325c..cbf97fc403 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -360,7 +360,7 @@ public function refresh($document) * @return object The loaded and managed document instance or NULL if the document can not be found. * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? */ - public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = array()) + public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null) { $criteria = $this->prepareQuery($criteria); $cursor = $this->collection->find($criteria)->limit(1); @@ -499,7 +499,7 @@ private function createDocument($result, $document = null, array $hints = array( $this->uow->registerManaged($document, $id, $result); } - return $this->uow->getOrCreateDocument($this->class->name, $result, $hints); + return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document); } /** @@ -522,7 +522,7 @@ public function primeCollection(Iterator $collection, $fieldName, $primer, array $fieldMapping = $collectionMetaData->fieldMappings[$fieldName]; $cmd = $this->cmd; - $groupedIds = array(); + $groupedQueries = array(); foreach ($collection as $element) { if ($fieldMapping['type'] == 'many') { @@ -539,10 +539,14 @@ public function primeCollection(Iterator $collection, $fieldName, $primer, array $id = (string) $mongoId; $document = $this->uow->tryGetById($id, $className); if (!$document || $document instanceof Proxy && ! $document->__isInitialized__) { - if ( ! isset($groupedIds[$className])) { - $groupedIds[$className] = array(); + if ( ! isset($groupedQueries[$className])) { + $groupedQueries[$className] = array(); + } + if ($document instanceof Proxy) { + $groupedQueries[$className][] = $document->__query__; + } else { + $groupedQueries[$className][] = $this->dm->getReferenceQueryFromDBRef($reference, $id, $this->dm->getClassMetadata($className), $fieldMapping); } - $groupedIds[$className][] = $mongoId; } } } @@ -550,23 +554,16 @@ public function primeCollection(Iterator $collection, $fieldName, $primer, array $document = $collectionMetaData->getFieldValue($element, $fieldName); if ($document && $document instanceof Proxy && ! $document->__isInitialized__) { $class = $this->dm->getClassMetadata(get_class($document)); - $groupedIds[$class->name][] = $this->uow->getDocumentIdentifier($document); + $groupedQueries[$class->name][] = $document->__query__; } } } - foreach ($groupedIds as $className => $ids) { - $class = $this->dm->getClassMetadata($className); + foreach ($groupedQueries as $className => $queries) { if ($primer instanceof \Closure) { - $primer($this->dm, $className, $fieldName, $ids, $hints); + $primer($this->dm, $className, $fieldName, $queries, $hints); } else { - $repository = $this->dm->getRepository($className); - $qb = $repository->createQueryBuilder() - ->field($class->identifier)->in($ids); - if (isset($hints[Query::HINT_SLAVE_OKAY])) { - $qb->slaveOkay(true); - } - $query = $qb->getQuery(); - $query->execute()->toArray(); + $qb = $this->createGroupedQueriesBuilder($className, $queries, $hints); + $qb->getQuery()->execute()->toArray(); } } } @@ -627,7 +624,7 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollection $col $hints = $collection->getHints(); $mapping = $collection->getMapping(); $cmd = $this->cmd; - $groupedIds = array(); + $groupedQueries = array(); foreach ($collection->getMongoData() as $key => $reference) { if (isset($mapping['simple']) && $mapping['simple']) { @@ -638,46 +635,34 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollection $col $mongoId = $reference[$cmd . 'id']; } $id = (string) $mongoId; - $reference = $this->dm->getReference($className, $id); + $query = $this->dm->getReferenceQueryFromDBRef($reference, $id, $this->dm->getClassMetadata($className), $mapping); + $reference = $this->dm->getReference($className, $id, $query); if ($mapping['strategy'] === 'set') { $collection->set($key, $reference); } else { $collection->add($reference); } if ($reference instanceof Proxy && ! $reference->__isInitialized__) { - if ( ! isset($groupedIds[$className])) { - $groupedIds[$className] = array(); + if ( ! isset($groupedQueries[$className])) { + $groupedQueries[$className] = array(); } - $groupedIds[$className][] = $mongoId; + $groupedQueries[$className][] = $query; } } - foreach ($groupedIds as $className => $ids) { - $class = $this->dm->getClassMetadata($className); - $mongoCollection = $this->dm->getDocumentCollection($className); - $criteria = array_merge( - array('_id' => array($cmd . 'in' => $ids)), - isset($mapping['criteria']) ? $mapping['criteria'] : array() - ); - $cursor = $mongoCollection->find($criteria); + foreach ($groupedQueries as $className => $queries) { + $qb = $this->createGroupedQueriesBuilder($className, $queries, $hints); + if (isset($mapping['sort'])) { - $cursor->sort($mapping['sort']); + $qb->sort($mapping['sort']); } if (isset($mapping['limit'])) { - $cursor->limit($mapping['limit']); + $qb->limit($mapping['limit']); } if (isset($mapping['skip'])) { - $cursor->skip($mapping['skip']); - } - if (isset($hints[Query::HINT_SLAVE_OKAY])) { - $cursor->slaveOkay(true); - } - $documents = $cursor->toArray(); - foreach ($documents as $documentData) { - $document = $this->uow->getById((string) $documentData['_id'], $class->rootDocumentName); - $data = $this->hydratorFactory->hydrate($document, $documentData); - $this->uow->setOriginalDocumentData($document, $data); - $document->__isInitialized__ = true; + $qb->skip($mapping['skip']); } + + $qb->getQuery()->execute()->toArray(); } } @@ -686,16 +671,12 @@ private function loadReferenceManyCollectionInverseSide(PersistentCollection $co $hints = $collection->getHints(); $mapping = $collection->getMapping(); $owner = $collection->getOwner(); - $ownerClass = $this->dm->getClassMetadata(get_class($owner)); - $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); - $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']]; - $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'].'.id'; - $criteria = array_merge( - array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)), - isset($mapping['criteria']) ? $mapping['criteria'] : array() - ); - $qb = $this->dm->createQueryBuilder($mapping['targetDocument']) - ->setQueryArray($criteria); + + $qb = $this->dm->createQueryBuilder($mapping['targetDocument']); + if (isset($mapping['criteria'])) { + $qb->setQueryArray($mapping['criteria']); + } + $qb->field($mapping['mappedBy'])->references($owner); if (isset($mapping['sort'])) { $qb->sort($mapping['sort']); @@ -737,6 +718,46 @@ private function loadReferenceManyWithRepositoryMethod(PersistentCollection $col } } + /** + * Creates a query builder for the grouped queries. + * + * @param string $className + * @param array $queries + * @param array $hints + * + * @return \Doctrine\ODM\MongoDB\Query\Builder + */ + private function createGroupedQueriesBuilder($className, array $queries, array $hints) + { + $qb = $this->dm->createQueryBuilder($className); + + $query = current($queries); + if (is_scalar($query) || count($query) == 1) { + $fieldName = is_scalar($query) ? 'id' : key($query); + $qb->field($fieldName)->in($queries); + } else { + if (count($queries) == 1) { + foreach ($query as $fieldName => $value) { + $qb->field($fieldName)->equals($value); + } + } else { + foreach ($queries as $query) { + $expr = $qb->expr(); + foreach ($query as $fieldName => $value) { + $expr->field($fieldName)->equals($value); + } + $qb->addOr($expr); + } + } + } + + if (isset($hints[Query::HINT_SLAVE_OKAY])) { + $qb->slaveOkay(true); + } + + return $qb; + } + /** * Gets the query for the document. * diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/ProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/ProxyFactory.php index c6de0d52b8..bdd9f06ac3 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/ProxyFactory.php @@ -86,9 +86,11 @@ public function __construct(DocumentManager $dm, $proxyDir, $proxyNs, $autoGener * * @param string $className * @param mixed $identifier + * @param mixed $query + * @param array $sort * @return object */ - public function getProxy($className, $identifier) + public function getProxy($className, $identifier, $query, array $sort = null) { $fqn = self::generateProxyClassName($className, $this->proxyNamespace); @@ -106,7 +108,7 @@ public function getProxy($className, $identifier) $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($className); - return new $fqn($documentPersister, $identifier); + return new $fqn($documentPersister, $identifier, $query, $sort); } /** @@ -251,7 +253,7 @@ private function generateMethods(ClassMetadata $class) if ($this->isShortIdentifierGetter($method, $class)) { $identifier = lcfirst(substr($method->getName(), 3)); - $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; + $methods .= ' if ($this->__isInitialized__ === false && null !== $this->__identifier__) {' . "\n"; $methods .= ' return $this->__identifier__;' . "\n"; $methods .= ' }' . "\n"; } @@ -350,10 +352,14 @@ class extends \ implements \Doctrine\ODM\MongoDB\Pro private $__documentPersister__; public $__identifier__; public $__isInitialized__ = false; - public function __construct(DocumentPersister $documentPersister, $identifier) + public $__query__; + public $__sort__; + public function __construct(DocumentPersister $documentPersister, $identifier, $query, $sort) { $this->__documentPersister__ = $documentPersister; $this->__identifier__ = $identifier; + $this->__query__ = $query; + $this->__sort__ = $sort; } /** @private */ public function __load() @@ -368,10 +374,10 @@ public function __load() $this->__wakeup(); } - if ($this->__documentPersister__->load($this->__identifier__, $this) === null) { - throw \Doctrine\ODM\MongoDB\DocumentNotFoundException::documentNotFound(get_class($this), $this->__identifier__); + if ($this->__documentPersister__->load($this->__query__, $this, array(), 0, $this->__sort__) === null) { + throw \Doctrine\ODM\MongoDB\DocumentNotFoundException::documentNotFound(get_class($this), $this->__query__); } - unset($this->__documentPersister__, $this->__identifier__); + unset($this->__documentPersister__, $this->__identifier__, $this->__query__, $this->__sort__); } } @@ -393,14 +399,14 @@ public function __clone() if (!$this->__isInitialized__ && $this->__documentPersister__) { $this->__isInitialized__ = true; $class = $this->__documentPersister__->getClassMetadata(); - $original = $this->__documentPersister__->load($this->__identifier__); + $original = $this->__documentPersister__->load($this->__query__, null, array(), 0, $this->__sort__); if ($original === null) { - throw \Doctrine\ODM\MongoDB\MongoDBException::documentNotFound(get_class($this), $this->__identifier__); + throw \Doctrine\ODM\MongoDB\MongoDBException::documentNotFound(get_class($this), $this->__query__); } - foreach ($class->reflFields AS $field => $reflProperty) { + foreach ($class->reflFields as $field => $reflProperty) { $reflProperty->setValue($this, $reflProperty->getValue($original)); } - unset($this->__documentPersister__, $this->__identifier__); + unset($this->__documentPersister__, $this->__identifier__, $this->__query__, $this->__sort__); } } diff --git a/lib/Doctrine/ODM/MongoDB/Query/Expr.php b/lib/Doctrine/ODM/MongoDB/Query/Expr.php index 908a01c298..3afdbd03e9 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Expr.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Expr.php @@ -63,23 +63,16 @@ public function references($document) { if ($this->currentField) { $mapping = $this->class->getFieldMapping($this->currentField); - $dbRef = $this->dm->createDBRef($document, $mapping); - + $dbRef = $this->dm->createDBRef($document, $mapping, true); if (isset($mapping['simple']) && $mapping['simple']) { - $this->query[$mapping['name']] = $dbRef; + $this->query[$this->currentField] = $dbRef; } else { - $keys = array('ref' => true, 'id' => true, 'db' => true); - - if (isset($mapping['targetDocument'])) { - unset($keys['ref'], $keys['db']); - } - - foreach ($keys as $key => $value) { - $this->query[$this->currentField . '.' . $this->cmd . $key] = $dbRef[$this->cmd . $key]; + foreach ($dbRef as $key => $value) { + $this->query[$this->currentField . '.' . $key] = $value; } } } else { - $dbRef = $this->dm->createDBRef($document); + $dbRef = $this->dm->createDBRef($document, null, true); $this->query = $dbRef; } @@ -93,23 +86,9 @@ public function includesReferenceTo($document) { if ($this->currentField) { $mapping = $this->class->getFieldMapping($this->currentField); - $dbRef = $this->dm->createDBRef($document, $mapping); - - if (isset($mapping['simple']) && $mapping['simple']) { - $this->query[$mapping['name']][$this->cmd . 'elemMatch'] = $dbRef; - } else { - $keys = array('ref' => true, 'id' => true, 'db' => true); - - if (isset($mapping['targetDocument'])) { - unset($keys['ref'], $keys['db']); - } - - foreach ($keys as $key => $value) { - $this->query[$this->currentField][$this->cmd . 'elemMatch'][$this->cmd . $key] = $dbRef[$this->cmd . $key]; - } - } + $this->query[$this->currentField][$this->cmd . 'elemMatch'] = $this->dm->createDBRef($document, $mapping, true); } else { - $dbRef = $this->dm->createDBRef($document); + $dbRef = $this->dm->createDBRef($document, null, true); $this->query[$this->cmd . 'elemMatch'] = $dbRef; } diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 91a8bc2f75..4a09d22dec 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -2434,10 +2434,11 @@ public function isCollectionScheduledForDeletion(PersistentCollection $coll) * @param string $className The name of the document class. * @param array $data The data for the document. * @param array $hints Any hints to account for during reconstitution/lookup of the document. + * @param object $document The document object to fill, if any. * @return object The document instance. * @internal Highly performance-sensitive method. */ - public function getOrCreateDocument($className, $data, &$hints = array()) + public function getOrCreateDocument($className, $data, &$hints = array(), $document = null) { $class = $this->dm->getClassMetadata($className); @@ -2451,8 +2452,12 @@ public function getOrCreateDocument($className, $data, &$hints = array()) } $id = $class->getPHPIdentifierValue($data['_id']); - if (isset($this->identityMap[$class->rootDocumentName][$id])) { - $document = $this->identityMap[$class->rootDocumentName][$id]; + if ($document || isset($this->identityMap[$class->rootDocumentName][$id])) { + if (!$document) { + // use the managed document + $document = $this->identityMap[$class->rootDocumentName][$id]; + } + $oid = spl_object_hash($document); if ($document instanceof Proxy && ! $document->__isInitialized__) { $document->__isInitialized__ = true; diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardingTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardingTest.php new file mode 100644 index 0000000000..e3a05ff44f --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardingTest.php @@ -0,0 +1,316 @@ +shardKey = $shardKey = 'foo'; + $shard->name = 'bar'; + + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $shardRef->simpleShardReference = $shard; + + $this->dm->persist($shard); + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->hydrate(false) + ->getQuery() + ->getSingleResult(); + + $this->assertTrue(isset($shardRef['simpleShardReference']['$sk'])); + $this->assertEquals($shardKey, $shardRef['simpleShardReference']['$sk']); + } + + public function testSimpleShardReference() + { + $shard = new SimpleShardDocument(); + $shard->shardKey = $shardKey = 'foo'; + $shard->name = 'bar'; + + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $shardRef->simpleShardReference = $shard; + + $this->dm->persist($shard); + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->getQuery() + ->getSingleResult(); + + $this->assertEquals($shardKey, $shardRef->simpleShardReference->getShardKey()); + } + + public function testComplexShardDBRef() + { + $shard = new ComplexShardDocument(); + $shard->shardKey = $shardKey = 'foo'; + $shard->name = $name = 'bar'; + $shard->text = $text = 'test'; + + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $shardRef->complexShardReference = $shard; + + $this->dm->persist($shard); + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->hydrate(false) + ->getQuery() + ->getSingleResult(); + + $this->assertTrue(isset($shardRef['complexShardReference']['$sk'])); + $this->assertEquals($shardKey, $shardRef['complexShardReference']['$sk']); + $this->assertTrue(isset($shardRef['complexShardReference']['$n'])); + $this->assertEquals($name, $shardRef['complexShardReference']['$n']); + $this->assertTrue(isset($shardRef['complexShardReference']['$text'])); + $this->assertEquals($text, $shardRef['complexShardReference']['$text']); + } + + public function testComplexShardReference() + { + $shard = new ComplexShardDocument(); + $shard->shardKey = $shardKey = 'foo'; + $shard->name = $name = 'bar'; + $shard->text = $text = 'test'; + + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $shardRef->complexShardReference = $shard; + + $this->dm->persist($shard); + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->getQuery() + ->getSingleResult(); + + $this->assertEquals($shardKey, $shardRef->complexShardReference->getShardKey()); + $this->assertEquals($name, $shardRef->complexShardReference->getName()); + $this->assertEquals($text, $shardRef->complexShardReference->getText()); + } + + public function testManySimpleShardDBRef() + { + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $numShards = 2; + for($i = 1; $i <= $numShards; $i++) { + $shard = new SimpleShardDocument(); + $shard->shardKey = 'foo' . $i; + $shard->name = 'bar' . $i; + + $shardRef->shardReferences[] = $shard; + } + + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->hydrate(false) + ->getQuery() + ->getSingleResult(); + + $this->assertEquals($numShards, count($shardRef['shardReferences'])); + for($i = 0; $i < $numShards; $i++) { + $this->assertTrue(isset($shardRef['shardReferences'][$i]['$sk'])); + $this->assertEquals('foo' . ($i + 1), $shardRef['shardReferences'][$i]['$sk']); + } + } + + public function testManySimpleShardReferences() + { + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $numShards = 2; + for($i = 1; $i <= $numShards; $i++) { + $shard = new SimpleShardDocument(); + $shard->shardKey = 'foo' . $i; + $shard->name = 'bar' . $i; + + $shardRef->shardReferences[] = $shard; + } + + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->getQuery() + ->getSingleResult(); + + $this->assertEquals($numShards, count($shardRef->shardReferences)); + for($i = 0; $i < $numShards; $i++) { + $this->assertEquals('foo' . ($i + 1), $shardRef->shardReferences[$i]->getShardKey()); + } + } + + public function testEmbeddedShardDBRef() + { + $this->markTestIncomplete('Embedded shard keys not supported.'); + + $embedShard = new EmbeddedShardKeyDocument(); + $embedShard->shardKey = $shardKey = 'foo'; + + $shard = new EmbeddedShardDocument(); + $shard->name = 'bar'; + $shard->embedOne = $embedShard; + + $shardRef = new ShardRefDocument(); + $shardRef->name = 'foo'; + $shardRef->embeddedShardReference = $shard; + + $this->dm->persist($shard); + $this->dm->persist($shardRef); + $this->dm->flush(); + $this->dm->clear(); + + $shardRef = $this->dm->createQueryBuilder('Doctrine\ODM\MongoDB\Tests\Functional\ShardRefDocument') + ->field('name')->equals('foo') + ->hydrate(false) + ->getQuery() + ->getSingleResult(); + + $this->assertTrue(isset($shardRef['embeddedShardReference']['$embedOne.sk'])); + $this->assertEquals($shardKey, $shardRef['embeddedShardReference']['$embedOne.sk']); + } +} + +/** + * @ODM\Document + */ +class ShardRefDocument +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String */ + public $name; + + /** @ODM\ReferenceOne(targetDocument="Doctrine\ODM\MongoDB\Tests\Functional\SimpleShardDocument") */ + public $simpleShardReference; + + /** @ODM\ReferenceOne(targetDocument="Doctrine\ODM\MongoDB\Tests\Functional\ComplexShardDocument") */ + public $complexShardReference; + + /** @ODM\ReferenceMany(targetDocument="Doctrine\ODM\MongoDB\Tests\Functional\SimpleShardDocument", cascade={"all"}) */ + public $shardReferences = array(); + + /** @ODM\ReferenceOne(targetDocument="Doctrine\ODM\MongoDB\Tests\Functional\EmbeddedShardDocument") */ + public $embeddedShardReference; +} + +/** + * @ODM\Document + * @ODM\QueryFields({"shardKey"}) + */ +class SimpleShardDocument +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String(name="sk") */ + public $shardKey; + + /** @ODM\String */ + public $name; + + public function getShardKey() + { + return $this->shardKey; + } +} + +/** + * @ODM\Document + * @ODM\QueryFields({"shardKey", "name", "text"}) + */ +class ComplexShardDocument +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String(name="sk") */ + public $shardKey; + + /** @ODM\String(name="n") */ + public $name; + + /** @ODM\String */ + public $text; + + public function getShardKey() + { + return $this->shardKey; + } + + public function getName() + { + return $this->name; + } + + public function getText() + { + return $this->text; + } +} + +/** + * @ODM\Document + * @ODM\QueryFields({"embedOne.shardKey"}) + */ +class EmbeddedShardDocument +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String(name="n") */ + public $name; + + /** @ODM\EmbedOne(targetDocument="Doctrine\ODM\MongoDB\Tests\Functional\EmbeddedShardKeyDocument") */ + public $embedOne; + + public function getName() + { + return $this->name; + } + + public function getEmbedOne() + { + return $this->embedOne; + } +} + +/** + * @ODM\EmbeddedDocument + */ +class EmbeddedShardKeyDocument +{ + /** @ODM\String(name="sk") */ + public $shardKey; +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Query/ExprTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Query/ExprTest.php index 5c78f9f5b3..b3ff3a88c0 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Query/ExprTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Query/ExprTest.php @@ -148,7 +148,7 @@ public function testReferencesUsesMinimalKeys() $dm->expects($this->once()) ->method('createDBRef') - ->will($this->returnValue(array('$ref' => 'coll', '$id' => '1234', '$db' => 'db'))); + ->will($this->returnValue(array('$id' => '1234'))); $dm->expects($this->once()) ->method('getUnitOfWork') ->will($this->returnValue($uw)); @@ -210,4 +210,4 @@ public function testReferencesUsesAllKeys() $this->assertEquals($expected, $expr->getQuery(), '->references() uses all keys if no targetDocument is set'); } -} \ No newline at end of file +}