Skip to content

Commit

Permalink
[MODM-105] Adding much improved reference support. Added mappedBy and…
Browse files Browse the repository at this point in the history
… inversedBy for better performing references instead of always storing references on both sides.
  • Loading branch information
jwage committed Feb 21, 2011
1 parent 3bf5d44 commit d1bffbd
Show file tree
Hide file tree
Showing 24 changed files with 874 additions and 20 deletions.
36 changes: 35 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName,
$mapping['fieldName'],
Type::getType($mapping['type'])->closureToPHP()
);
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE) {
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
$code .= sprintf(<<<EOF
/** @ReferenceOne */
Expand All @@ -222,6 +222,40 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName,
$mapping['name'],
$mapping['fieldName']
);
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
if ($mapping['repositoryMethod']) {
$code .= sprintf(<<<EOF
\$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
\$return = \$this->dm->getRepository(\$className)->%3\$s();
\$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
\$hydratedData['%2\$s'] = \$return;
EOF
,
$mapping['name'],
$mapping['fieldName'],
$mapping['repositoryMethod']
);
} else {
$code .= sprintf(<<<EOF
\$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
\$criteria = array_merge(
array(\$this->class->fieldMappings['%2\$s']['mappedBy'] . '.\$id' => \$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);
\$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
\$hydratedData['%2\$s'] = \$return;
EOF
,
$mapping['name'],
$mapping['fieldName']
);
}
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
$code .= sprintf(<<<EOF
Expand Down
12 changes: 12 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,18 @@ public function mapField(array $mapping)
if (isset($mapping['lock'])) {
$this->setLockMapping($mapping);
}
$mapping['isOwningSide'] = true;
$mapping['isInverseSide'] = false;
if (isset($mapping['reference'])) {
if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
$mapping['isOwningSide'] = true;
$mapping['isInverseSide'] = false;
}
if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
$mapping['isInverseSide'] = true;
$mapping['isOwningSide'] = false;
}
}
$this->fieldMappings[$mapping['fieldName']] = $mapping;
return $mapping;
}
Expand Down
14 changes: 14 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/Driver/DoctrineAnnotations.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ final class ReferenceOne extends Field
public $discriminatorField;
public $discriminatorMap;
public $cascade;
public $inversedBy;
public $mappedBy;
public $repositoryMethod;
public $sort = array();
public $criteria = array();
public $limit;
public $skip;
}
final class ReferenceMany extends Field
{
Expand All @@ -185,6 +192,13 @@ final class ReferenceMany extends Field
public $discriminatorField;
public $discriminatorMap;
public $cascade;
public $inversedBy;
public $mappedBy;
public $repositoryMethod;
public $sort = array();
public $criteria = array();
public $limit;
public $skip;
public $strategy = 'pushAll'; // pushAll, set
}
class NotSaved extends Field {}
Expand Down
13 changes: 9 additions & 4 deletions lib/Doctrine/ODM/MongoDB/PersistentCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ private function changed()
{
if ( ! $this->isDirty) {
$this->isDirty = true;
if ($this->dm && $this->mapping !== null && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
if ($this->dm && $this->mapping !== null && $this->mapping['isOwningSide'] && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
$this->uow->scheduleForDirtyCheck($this->owner);
}
}
Expand Down Expand Up @@ -408,6 +408,9 @@ public function getValues()
*/
public function count()
{
if ($this->mapping['isInverseSide']) {
$this->initialize();
}
return count($this->mongoData) + $this->coll->count();
}

Expand Down Expand Up @@ -508,9 +511,11 @@ public function clear()
}
$this->mongoData = array();
$this->coll->clear();
$this->changed();
$this->uow->scheduleCollectionDeletion($this);
$this->takeSnapshot();
if ($this->mapping['isOwningSide']) {
$this->changed();
$this->uow->scheduleCollectionDeletion($this);
$this->takeSnapshot();
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfW
*/
public function delete(PersistentCollection $coll, array $options)
{
$mapping = $coll->getMapping();
if ($mapping['isInverseSide']) {
return; // ignore inverse side
}
list($propertyPath, $parent) = $this->getPathAndParent($coll);
$query = array($this->cmd . 'unset' => array($propertyPath => true));
$this->executeQuery($parent, $query, $options);
Expand All @@ -101,6 +105,10 @@ public function delete(PersistentCollection $coll, array $options)
*/
public function update(PersistentCollection $coll, array $options)
{
$mapping = $coll->getMapping();
if ($mapping['isInverseSide']) {
return; // ignore inverse side
}
$this->deleteRows($coll, $options);
$this->insertRows($coll, $options);
}
Expand Down
97 changes: 91 additions & 6 deletions lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventMa
$this->collection = $dm->getDocumentCollection($class->name);
}

public function getInserts()
{
return $this->queuedInserts;
}

public function isQueuedForInsert($document)
{
return isset($this->queuedInserts[spl_object_hash($document)]) ? true : false;
}

/**
* Adds a document to the queued insertions.
* The document remains queued until {@link executeInserts} is invoked.
Expand Down Expand Up @@ -324,13 +334,18 @@ public function refresh($id, $document)
* a new document is created.
* @param array $hints Hints for document creation.
* @param int $lockMode
* @param array $sort
* @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)
public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = array())
{
$criteria = $this->prepareQuery($criteria);
$result = $this->collection->findOne($criteria);
$cursor = $this->collection->find($criteria)->limit(1);
if ($sort) {
$cursor->sort($sort);
}
$result = $cursor->getSingleResult();

if ($this->class->isLockable) {
$lockMapping = $this->class->fieldMappings[$this->class->lockField];
Expand Down Expand Up @@ -457,7 +472,16 @@ public function loadCollection(PersistentCollection $collection)
break;

case ClassMetadata::REFERENCE_MANY:
$this->loadReferenceManyCollection($collection);
$mapping = $collection->getMapping();
if ($mapping['repositoryMethod']) {
$this->loadReferenceManyWithRepositoryMethod($collection);
} else {
if ($mapping['isOwningSide']) {
$this->loadReferenceManyCollectionOwningSide($collection);
} else {
$this->loadReferenceManyCollectionInverseSide($collection);
}
}
break;
}
}
Expand All @@ -481,7 +505,7 @@ private function loadEmbedManyCollection(PersistentCollection $collection)
}
}

private function loadReferenceManyCollection(PersistentCollection $collection)
private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection)
{
$mapping = $collection->getMapping();
$cmd = $this->cmd;
Expand All @@ -502,15 +526,76 @@ private function loadReferenceManyCollection(PersistentCollection $collection)
foreach ($groupedIds as $className => $ids) {
$class = $this->dm->getClassMetadata($className);
$mongoCollection = $this->dm->getDocumentCollection($className);
$data = $mongoCollection->find(array('_id' => array($cmd . 'in' => $ids)));
foreach ($data as $documentData) {
$criteria = array_merge(
array('_id' => array($cmd . 'in' => $ids)),
$mapping['criteria']
);
$cursor = $mongoCollection->find($criteria);
if ($mapping['sort']) {
$cursor->sort($mapping['sort']);
}
if ($mapping['limit']) {
$cursor->limit($mapping['limit']);
}
if ($mapping['skip']) {
$cursor->skip($mapping['skip']);
}
foreach ($cursor as $documentData) {
$document = $this->uow->getById((string) $documentData['_id'], $class->rootDocumentName);
$data = $this->hydratorFactory->hydrate($document, $documentData);
$this->uow->setOriginalDocumentData($document, $data);
}
}
}

private function loadReferenceManyCollectionInverseSide(PersistentCollection $collection)
{
$mapping = $collection->getMapping();
$owner = $collection->getOwner();
$class = $this->dm->getClassMetadata($mapping['targetDocument']);
$mongoCollection = $this->dm->getDocumentCollection($mapping['targetDocument']);
$criteria = array_merge(
array($mapping['mappedBy'].'.'.$this->cmd.'id' => $class->getIdentifierObject($owner)),
$mapping['criteria']
);
$cursor = $mongoCollection->find($criteria);
if ($mapping['sort']) {
$cursor->sort($mapping['sort']);
}
if ($mapping['limit']) {
$cursor->limit($mapping['limit']);
}
if ($mapping['skip']) {
$cursor->skip($mapping['skip']);
}
foreach ($cursor as $documentData) {
$document = $this->dm->getReference($class->name, (string) $documentData['_id']);

This comment has been minimized.

Copy link
@tecbot

tecbot Feb 24, 2011

the discriminator map for inverse side, doesn't work.

This comment has been minimized.

Copy link
@jwage

jwage Feb 24, 2011

Author Member

If you think about it, the inverse side discriminator map cannot work. How could it? we no longer store references on the inverse side if you specify mappedBy

This comment has been minimized.

Copy link
@tecbot

tecbot Feb 24, 2011

i mean the discriminator map for the class and this is stored in the document, but the returned document from doctrine is only the document wich declare the discriminator map, because it use only the targetDocument option.

This comment has been minimized.

Copy link
@jwage

jwage Feb 24, 2011

Author Member

But like I said, it is impossible to discriminate which class to use because we do not store a reference in the database for the inverse side, so we have no where to store the discriminator value, which is used to lookup what class to instantiate. If you want to use inverse and owning side you cannot use the discriminator map on the inverse side.

This comment has been minimized.

Copy link
@tecbot

tecbot Feb 24, 2011

I think you dont understand me.

When i load a document with find and the class has a discriminator map. e.g:

@mongodb:Document()
@mongodb:InheritanceType("SINGLE_COLLECTION")
@mongodb:DiscriminatorMap({
    "foo" = "Bar",
    "bar" = "Foo"
})
class Foo
{
     // ....
}

Then doctrine return the right class Bar or Foo for a id.

But wenn the id loaded with the loadReferenceManyCollectionInverseSide function, then is the class only Foo.

This comment has been minimized.

Copy link
@jwage

jwage Feb 24, 2011

Author Member

I understand now. I thought you meant the discriminator map on the reference. This is why test cases are important! :)

This comment has been minimized.

Copy link
@tecbot

tecbot Feb 24, 2011

hehe thats right ;).

This comment has been minimized.

Copy link
@jwage

jwage Feb 24, 2011

Author Member

8bb7d01

I think this should take care of it.

if ($document instanceof Proxy && ! $document->__isInitialized__) {
$data = $this->hydratorFactory->hydrate($document, $documentData);
$this->uow->setOriginalDocumentData($document, $data);
}
$collection->add($document);
}
}

private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection)
{
$mapping = $collection->getMapping();
$cursor = $this->dm->getRepository($mapping['targetDocument'])->$mapping['repositoryMethod']();
if ($mapping['sort']) {
$cursor->sort($mapping['sort']);
}
if ($mapping['limit']) {
$cursor->limit($mapping['limit']);
}
if ($mapping['skip']) {
$cursor->skip($mapping['skip']);
}
foreach ($cursor as $document) {
$collection->add($document);
}
}

/**
* Prepares a query array by converting the portable Doctrine types to the types mongodb expects.
*
Expand Down
15 changes: 13 additions & 2 deletions lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,13 @@ public function prepareInsertData($document)

// @ReferenceOne
} elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
if ($mapping['isInverseSide']) {
continue;
}

$oid = spl_object_hash($new);
if (isset($this->queuedInserts[$oid]) || $this->uow->isScheduledForInsert($new)) {

if ($this->isScheduledForInsert($new)) {
// The associated document $new is not yet persisted, so we must
// set $new = null, in order to insert a null value and schedule an
// extra update on the UnitOfWork.
Expand Down Expand Up @@ -230,7 +235,7 @@ public function prepareUpdateData($document)
}

// @ReferenceOne
} elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
} elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
if (isset($new) || $mapping['nullable'] === true) {
$updateData[$this->cmd . 'set'][$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
} else {
Expand Down Expand Up @@ -323,4 +328,10 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo
}
return $embeddedDocumentValue;
}

private function isScheduledForInsert($document)
{
return $this->uow->isScheduledForInsert($document)
|| $this->uow->getDocumentPersister(get_class($document))->isQueuedForInsert($document);
}
}
Loading

0 comments on commit d1bffbd

Please sign in to comment.