Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add generic reference as replacement for DBRef #1623

Merged
merged 6 commits into from
Oct 7, 2017

Conversation

alcaeus
Copy link
Member

@alcaeus alcaeus commented Jul 26, 2017

Supersedes #1566.

As discussed in our last hangout, this is what remains of the DBRef replacement. DBRef is not deprecated (yet), athough we might decide to deprecate it at this time. At this time the PR is feature complete, I'd like to write a whole bunch of tests to ensure the whole discriminator handling/omitting target collection name works as I'd expect it to.

The new reference object only stores an ID and optionally a discriminator field. The main difference to DBRef is that it can be used in aggregation pipeline stages that don't handle DBRef objects because of the $ in field names (e.g. $lookup and $graphLookup).

I'll also revisit #1527 to see if we can store additional fields in the reference (which would also apply to DBRef objects).

references are not compatible with the discriminators.
storeAs - Indicates how to store the reference. ``id`` stores the identifier,
``ref`` an embedded object containing the ``id`` field and (optionally) a
discriminator. ``dbRef`` and ``dbRefWithDb`` store a ``DBRef`` object. They
Copy link
Member

Choose a reason for hiding this comment

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

Should we keep the DBRef documentation link?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we should. Will update that.

``dbRef`` uses a `DBRef`_ without ``$db`` value and ``dbRefWithDb`` stores
a full `DBRef`_ (``$ref``, ``$id``, and ``$db``). Note that ``id``
``ref`` stores fields ``id`` and ``ref`` (similar to DBRef without $ prefix) value and ``refWithDb`` stores
a additionally db as parameter. ``dbRef`` and ``dbRefWithDb`` use ``DBRef``, they are deprecated in favor of ref and refWithDb. Note that ``id``
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason for this to not simply repeat the @ReferenceMany documentation for storeAs?

@@ -360,6 +360,7 @@ The ``storeAs`` option has three possible values:

- **dbRefWithDb**: Uses a `DBRef`_ with ``$ref``, ``$id``, and ``$db`` fields (this is the default)
- **dbRef**: Uses a `DBRef`_ with ``$ref`` and ``$id``
- **ref**: Uses a custom embedded object with an ``id`` field
- **id**: Uses a ``MongoId``
Copy link
Member

Choose a reason for hiding this comment

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

The id documentation should likely be changed to "Uses the identifier" instead of "MongoId" to be consistent with the change you made for @ReferenceMany and storeAs.

break;

default:
throw MappingException::cannotLookupNonIdReference($this->class->name, $fieldName);
Copy link
Member

Choose a reason for hiding this comment

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

Since this is repeated for $referenceMapping['storeAs'] above and $mappedByMapping['storeAs'] here, would it make sense to extract this to a helper method?

$dbRef = array(
'$ref' => $class->getCollection(),
'$id' => $class->getDatabaseIdentifierValue($id),
);
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit rusty, but is DocumentManager::createDBRef() to generic method for creating a reference to another document? I assume it's no longer specific to DBRef objects since the introduction of simple references some time ago.

Copy link
Member Author

Choose a reason for hiding this comment

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

The name would suggest it creates a DBRef object, but it was used to create whatever reference was required according to the mapping. I'd suggest deprecating this and introducing a new createReference method to take its place.

Copy link
Member Author

Choose a reason for hiding this comment

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

Introduced createReference which also makes the $referenceMapping argument mandatory. I've also replaced all occurences of createDBRef with createReference, hoping I didn't create a BC break with that because people expect createDBRef to be called.

@@ -722,7 +722,7 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollectionInter
$mongoId = $reference;
} else {
$className = $this->uow->getClassNameForAssociation($mapping, $reference);
$mongoId = $reference['$id'];
$mongoId = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
Copy link
Member

Choose a reason for hiding this comment

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

Earlier, I noted that you may want to add the array type hint to getReferenceId()'s first parameter. Alternatively, it may be worth changing getReferenceId() and UnitOfWork::getClassNameForAssociation() to work on simple references, which would let you remove this if/else entirely and simply assign $className and $mongoId the same for all reference types.

if (isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
$mappedByFieldName = $mapping['mappedBy'];
} else {
$mappedByFieldName = $mapping['mappedBy'] . '.' . ClassMetadataInfo::getReferencePrefix(isset($mappedByMapping['storeAs']) ? $mappedByMapping['storeAs'] : null) . 'id';
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, perhaps we can have a single method here that returns the mappedByFieldName for any reference type. That may allow us to keep getReferencePrefix() private.

@@ -1424,20 +1429,25 @@ private function getWriteOptions(array $options = array())
private function prepareDbRefElement($fieldName, $value, array $mapping, $inNewObj)
{
$dbRef = $this->dm->createDBRef($value, $mapping);
if ($inNewObj) {
if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't quite recall what prepareDbRefElement() is used for, but these changes appear to be correct. I assume discriminator values (if applicable) are added later, since this method only deals with basic keys for database references?

@@ -255,6 +255,7 @@ private function addManyReferences(PersistentCollectionInterface $persistentColl
{
$mapping = $persistentCollection->getMapping();


Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary line break?

public function testLookupStageReferenceManyStoreAsRef()
{
$this->requireMongoDB32('$lookup tests require at least MongoDB 3.2.0');
$this->insertTestData();
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense for this to go in the setUp() function for the test class, or is it not needed by each test case?

@alcaeus
Copy link
Member Author

alcaeus commented Sep 24, 2017

@jmikola took some time to work on this and applied most of your feedback. I still need to check for discriminator handling in prepareDBRef (which was renamed to prepareReference in DocumentPersister. I'll also have to revisit the exception mentioned in #1623 (comment) and add some tests. I'll hopefully get to that in a few days, but I believe the pull request is worth another look in the meantime.

@alcaeus
Copy link
Member Author

alcaeus commented Sep 25, 2017

Note: build fails due to new driver version. I'll take a look at that in master branch.

storeAs - Indicates how to store the reference. ``id`` stores the identifier,
``ref`` an embedded object containing the ``id`` field and (optionally) a
discriminator. ``dbRef`` and ``dbRefWithDb`` store a `DBRef`_ object. They
are deprecated in favor of ``ref``. Note that ``id`` references are not
Copy link
Member

Choose a reason for hiding this comment

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

For clarity, I'd replace ". They" with "and" to combine these sentences.

-
cascade - Cascade Option
-
discriminatorField - The field name to store the discriminator value within
the `DBRef`_ object.
the reference object.
Copy link
Member

Choose a reason for hiding this comment

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

Should this specify the logical conflict with id mapping, or is that discussed elsewhere? I assume we throw an exception for such mappings at runtime in any event.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's specified where we explain the different storeAs options:

Note that id references are not compatible with the discriminators.

I can add another note to each discriminator option for clarity.

switch ($storeAs) {
case ClassMetadataInfo::REFERENCE_STORE_AS_ID:
if ($class->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION) {
throw MappingException::simpleReferenceMustNotTargetDiscriminatedDocument($referenceMapping['targetDocument']);
Copy link
Member

Choose a reason for hiding this comment

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

This appears to be the exception I was expecting for the id strategy and discriminated reference conflict.

$dbRef = ['id' => $class->getDatabaseIdentifierValue($id)];
break;

default:
Copy link
Member

Choose a reason for hiding this comment

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

Might be safer to explicitly handle both cases here and leave default: in place to throw an exception for an unsupported strategy. That keeps the code defensive in case we ever add more strategies down the line.



case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
$dbRef = ['id' => $class->getDatabaseIdentifierValue($id)];
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to rename this local variable to $ref, since this was the original name used for two DBRef strategies handled by this function?

@@ -73,28 +73,34 @@ public function references($document)
{
if ($this->currentField) {
$mapping = $this->getReferenceMapping();
$dbRef = $this->dm->createDBRef($document, $mapping);
$dbRef = $this->dm->createReference($document, $mapping);
Copy link
Member

Choose a reason for hiding this comment

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

Rename to $ref?

$keys = array('ref' => true, 'id' => true, 'db' => true);
if ($storeAs === ClassMetadataInfo::REFERENCE_STORE_AS_REF) {
$keys = ['id' => true];
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Another place we might want to be defensive about $storeAs values.

}
}
} else {
$dbRef = $this->dm->createDBRef($document);
@trigger_error('Calling ' . __METHOD__ . ' without a current field set will no longer be possible in ODM 2.0.', E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

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

Same question as before about role of @.

$keys = array('ref' => true, 'id' => true, 'db' => true);
if ($storeAs === ClassMetadataInfo::REFERENCE_STORE_AS_REF) {
$keys = ['id' => true];
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Defensive opportunity.

/**
* @before
*/
public function prepareTest()
Copy link
Member

Choose a reason for hiding this comment

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

Is this an alternative to setUp() and calling parent::setUp()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Exactly - not sure where the advantages of each are, but I've started using @before and @after instead of setUp and tearDown in test classes and only use the latter in abstract TestCase classes.

{
@trigger_error('The ' . __METHOD__ . ' method has been deprecated and will be removed in ODM 2.0. Use createReference() instead.', E_USER_DEPRECATED);

if (!isset($referenceMapping['storeAs'])) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be fixed on class metadata level?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since it's a public method I decided to be defensive regarding what we receive. There's no guarantee someone will pass an actual mapping, they could call it with a partial mapping array (e.g. our tests). One more reason to look forward to metadata objects.

$this->query[$mapping['name']] = $dbRef;
} else {
$keys = array('ref' => true, 'id' => true, 'db' => true);
switch ($storeAs) {
Copy link
Member

Choose a reason for hiding this comment

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

This seem to be very similar switch to one from DocumentPersister - maybe this should be merged somehow?

Copy link
Member Author

Choose a reason for hiding this comment

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

I can try and extract the logic to a common method - maybe I can use DocumentManager::createReference for it.

Copy link
Member

Choose a reason for hiding this comment

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

No big deal if it's complicated or anything, just occurred to me while reading :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Nah, it's worth a shot :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out the methods are similar to suggest extracting the logic to a separate method, but different enough to not really change anything. Decided to skip it after all.

$this->query[$mapping['name']] = $dbRef;
} else {
$keys = array('ref' => true, 'id' => true, 'db' => true);
switch ($storeAs) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here

@alcaeus alcaeus merged commit 845f07a into doctrine:master Oct 7, 2017
@alcaeus alcaeus deleted the add-new-reference-type branch October 7, 2017 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants