Problem
Allow multiple entity types in an EFQ reference.
- Core hard codes a field type, so contrib cannot implement EFQ joins.
- Contrib implementations can only support one target entity type
Proposed resolution
Field types supporting multiple target entity types need to know which entity type to join:
\Drupal::entityQuery('node')
->condition('erfield.entity:user.uid', '6', '=')
->execute();
\Drupal::entityQuery('node')
->condition('erfield.entity:node.title', 'foobar', '=')
->execute();
API addition
You can mention entity type just like context system
Field types supporting a single target entity type can use the existing syntax:
Drupal::entityQuery('node')
->condition('uid.entity.name', 'foobar', '=')
->execute();
Comments
Comment #1
dpiComment #2
dpiLooks like core may have hard coded ER in, may need core patch?
\Drupal\Core\Entity\Query\Sql\Tables
This is the error you get if you attempt to do the EFQ:
Comment #3
chx commentedComment #4
chx commentedComment #5
chx commentedComment #6
jibran@dpi can you try this patch? Thanks for the help @chx.
Comment #7
jibranThe fix in #6 is correct but it doesn't fix the issue for DER. We have to do it in followup issue for DER to fix that.
Here is a sample code I tried.
So
$entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId();in\Drupal\Core\Entity\Query\Sql\Tables:209is returning NULL cuz DER can't add that constraint to the entity property.Comment #8
chx commentedWell #7 is kinda impossible to fix within EQ because we need the entity properties across the relationship, i mean ->condition('ref_field.entity.foo', 'bar') well what's foo and where the heck it is stored? I understand of course that this is a problem for DER... but I do not have any good ideas on solving it.
Comment #9
dpiDo you think we can change the "entity" keyword to accept specific a specific entity type. For example hinting the type:
Current queries:
I've attached a working patch with this implementation:
This fixes all issues with the original Dynamic Entity Reference issue.
Comment #10
chx commentedWell, condition is based on the "real world" entity API.
$entity->author->entity->nameturns intocondition('author.entity.name'). I thought that neat.Comment #11
dpiER doesn't change at all, it still has the same syntax. DER, or other modules, just need a little nudge.
Other possible syntax ideas:
Comment #12
chx commentedI would vote on
condition('uid.user.name'). Check the issue summary for code.We already know we are looking for a relationship in this chunk of code; check a few lines above
// If there are more specifiers to come, it's a relationship.Comment #13
dpiPatch attached accepts both types:
These both valid for the same query. using entity forces a lookup of the field setting.
Comment #14
chx commentedFor the first part, if you were to change
$entity_type_id = $propertyDefinitions['entity']to$entity_type_id = $propertyDefinitions[$relationship_specifier]then you would have almost the same code as I suggested -- and for symmetry reason I still prefer mine.As for the fallback that the relationship specifier equals the entity id -- well, that would mean that you have
condition('fieldname.something.foo')where later$entity->something->foowouldn't work. It would also makecondition('uid.node.foo')work and wreak spectacular havoc -- the query would find nodes which IDs in place of users with those IDs. I do not think this is desirable.So no, I do not like #13 sorry.
Comment #15
chx commentedAnd to clarify: I am not against the whole idea, absolutely not, relation et al will rely on this, I am really quite glad we are working on this; I just would like to find a good solution. Thanks for perseverance!
Comment #16
dpiI agree, the symmetry is nice.
For the record, I'm totally fine with the original proposal. Option #2 would be a nicety as it frees contrib up a little and is effectively a two line change.
For others following...
Option #1
Use #6 with a sleight tweak to allow any property name.
This would require DER to implement additional computed properties implementing DataReferenceDefinitionInterface. Each new property would represent a valid entity type.
It also benefits the entity system as both of these are possible.
Option #2
Use a slightly magical version, where you do not need to specify properties for all entity types.
This would still work fine:
Comment #17
vijaycs85looks like option 1 in #16 is exactly what @chx proposed in issue summary. +1 that!
Comment #18
dpiIts probably a major? Added some eval in summary.
Comment #19
jibranOption #1
so @berdir said in IRC
But that mean
Yeah that is a big problem.
That will be a mess.
I know
node.entitydoesn't make sense for DER in EFQ(I agree with @chx point that the mapping is there for a reason) but for all practical purposes(entity api and field api) it makes a lot of sense cuz i don't have to worry which entity_type is it $node->field_der[0]->enitity it just gives me all information.While maintaining DER, my primary focus is to keep it in line with ER. So maybe we can replace core ER in D9 with DER. *wishful thinking* and this change will divert us so above change in DER is a no go. :(
Also @larowlan said in IRC it is an overkill and I agree.
Option #2
As @chx said
$node->uid->entity->nametranslate toWhich makes a lot of sense.
For DER we have two choices.
Either map
$node->field_der->entity->nametoOr
'field_der.entity:user.name'is not neat but we are already doing it in a lot of places is core plugin system and views are the major examples.'field_der.user.name'doesn't map to anything and it'll change the default behavior of EFQ and I wouldn't want that.@chx With all due respect, I would like you to reconsider
'field_der.entity:user.name'option because as far as I can see this is the only viable option. If not please suggest something else.Thank you @dpi for all the possible solution.
Comment #20
chx commentedIf we are not happy with
condition('derfield.node.uid')what about a small variant of #11:condition('derfield.entity(node).uid')? This makes it crystal clear that this is not part of the entity after as$entity->derfield->entity(node)->uidwill throw a fatal quite immediately. Also, the ordinary meaning of parentheses isSounds the exact situation here, it doesn't fit the normal flow but we want to include it.
Comment #21
jibranI have no problem with that.
Comment #22
chx commentedBut this is something that DER can decide. See the issue summary.
Comment #23
chx commentedComment #24
jibranSounds fair I'll create a followup for DER after this got fixed.
Comment #25
chx commentedLast shot for tonight: if you pass the $relationship_specifier to getTargetDefinition and add a little preg to getTargetDefinition then entity(node) entity*node entity#node and whatever else can be made to work in one go: preg_match('/^entity[^a-z0-9_]+([a-z0-9_]+)/', $relationship_specifier, $matches) will give you an entity id and then you just need to fish out the definition for that, for example return \Drupal::entityManager()->getDefinition($matches[1]) .
Comment #26
jibranSo now how we'll make
$propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterfacetrue for DER.Comment #27
chx commentedSo http://cgit.drupalcode.org/dynamic_entity_reference/tree/src/DataDynamic... @ 837d2df33e093491da325348cc6f9464d49d7704 has
class DataDynamicReferenceDefinition extends DataReferenceDefinitionand Drupal hasclass DataReferenceDefinition extends DataDefinition implements DataReferenceDefinitionInterfaceso your definition already implements that interface and I see DataDynamicReferenceDefinition being used in the field definition http://cgit.drupalcode.org/dynamic_entity_reference/tree/src/Plugin/Fiel... . What am I missing here?Comment #28
dpiDER doesnt need to change.
Comment #29
jibranSorry for not being clear.
$propertyDefinitions['entity'] instanceof DataReferenceDefinitionInterface is true for DER but
$propertyDefinitions['entity(node)'] instanceof DataReferenceDefinitionInterface is not true.
Comment #30
chx commentedIterated proposal then: f you specify entity#node then we will look at the property called entity and pass entity#node to the getTargetDefinition. Still whatever separator after entity is accepted.
I still do not want Tables to just blindly accept foo.node.bar without the foo field having any say whether it has a "node" so this is the best I could come up with. This adds back entity as a special string but it's only a default -- if the foo field does define a node field of a type implementing DataReferenceDefinitionInterface then foo.node.bar will work just fine. Notably even foo.entity_type.bar would work if entity_type is a reference item.
Comment #31
jibranThat works for me. Let's see how much this fails.
Comment #32
chx commentedComment #33
berdirI don't really get how this is supposed to work?
Shouldn't $property and $relationship_specifier be the other way round in the last changed line? Otherwise it would expect a property definition named 'node' to exist and then calls the method with entity(node)? Isn't this exactly what we want to avoid, otherwise we wouldn't have to do anything here?
I am also not sure that the original change here makes sense. The check would now for example also return TRUE for the language item reference, but that makes no sense, because that is not something you can query on. Like this:
condition('node.langcode.language.id'). I have no idea what that is even supposed to mean?
We *are* looking for an entity reference here, not any kind of reference, because we only support entity references.
The parameter and documentation isn't really explaining anything to me, or how I'm supposed to implement it.
I think that ^ shows that we somehow need tests for this, although I'm not quite sure how...
Comment #34
dpiSleight modification to #31:
Would still prefer it to take any property name, as long as it implements DataReferenceDefinitionInterface:
We're already doing the regex, so just pass along the text in brackets as argument to getTargetDefinition() instead of the whole string. e.g:
I didnt touch any of the argument naming of getTargetDefinition() from #33. That would need to be addressed.
Comment #35
dpiAddressed #33
Check for EntityDataDefinitionInterface implementation.
Added some more documentation to getTargetDefinition
The documentation for QueryInterface:Condition will need to be updated.
Comment #38
dpiUpdated to HEAD
Comment #39
chx commentedWhat is name(argument) ... ? I don't think I saw that in earlier patches.
Comment #40
dpi@chx I've updated the docs in this patch.. And original post.
entity_reference_field_name.entity_property_name(entity_type).column_on_entity_typeThe only (optional) addition is the brackets notation which you mentioned in #20.
Comment #41
dpiSince #35, patch only contains fast-forward and docs changes. See interdiff
Comment #44
dpiThat was odd
Comment #45
chx commented^([a-zA-Z_0-9]*){1}why this is not^([a-zA-Z_0-9]+){1} is redundant to the best of my regex knowledge and you want + not * because foo.(bar).baz is illegal.The getTargetDefinition doxygen is a bit weird, and $argument is also quite weird :/ Naming is hard. Maybe type_hint ? I would say something like "if multiple data types can be referenced by the object implementing this interface, pass this hint to get a relevant target definition" and the example ... well the example needs to move to the query interface because noone will find it here but it needs to carry a warning that core only supports single entity type references so (entity_type) is superflous.
Comment #46
matslats commentedI'm working on the realnames module.
The \Drupal\user\Plugin\EntityReferenceSelection\UserSelection handler builds an entity query which I would like to join to the realname db table.
I came here to file a request to un-protect property Drupal\Core\Entity\Query\Sql\Query::sqlQuery because otherwise I can see no way to add the join.
But I see I would also have to rewrite the basic conditions of the query too, because I need to filter on user.name OR realname.realname
I didn't understand anything at the top of this thread, and I don't know if this fix will help me.
Any thoughts?
Comment #47
amateescu commented@matslats,
\Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery()adds the 'entity_reference' tag to its entity query, along with the entire selection handler class as metadata, so you can just implementhook_query_TAG_alter()to alter the query. Note that in that hook, the query you receive as a parameter is a db query object, not the entity query frombuildEntityQuery().See
system_query_entity_reference_alter()for an example.Comment #48
matslats commentedThanks @amateescu hadn't thought of that.
I'm not seeking support in the wrong place but I want it to be noted here that that solution won't help in the realnames module because the entityQuery was initiatied with AND and by the time it reaches the alter phase, its conditions look like:
and I need to OR that with realnames.realname LIKE %ad%
i.e. gimme uids where user.name like %fragment% OR realnames.realname like %fragment%
To work around this I would have to build my own 'EntityReferenceSelection' plugin which didn't extend
\Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery
and which therefore built a query without
$query = $this->entityManager->getStorage($target_type)->getQuery();
Looks like a lot of code.
So will this patch offer a way to join an entityQuery to another table and also rework the conditions?
Comment #50
jibranWe only need to address #45 and then we are done.
Comment #51
jibranHere is the new and better version of the patch. Someone suggested to me to use
entity:nodeformat just like context system and then we don't have to fear about BC break and horrific regex. The patch only need -ve test but other then that every thing is working correctly.Thanks someone for all the help.
It is BC safe solution to moving it back to 8.1.x
DER can now use
->condition("field_der.target_id.entity:entity_test.name", 'Foobar')to mention the target_type.Comment #52
jibranComment #53
jibranHere is how DER can leverage this.
Comment #54
jibranUpdated issue summary.
Comment #56
jibranWe don't need -ve tests here because if the wrong entity type is used then the result set would be invalid and if entity type is not defined then entity manager would throw a
PluginNotFoundException.Comment #57
jibranHere some improvement after the discussion with @Berdir.
Comment #58
jibranComment #59
jibranComment #60
jibranComment #61
berdirnitpick: double space
.. that is *the* only one..
I also don't think that 0th account isn't really a thing. We start counting with 0, but that's still the 1st account?
This only adds tests for one part of this issue, the new "feature--task-bug" of being able to specificy the entity type, which is useful for DER but pretty meaningless for core. The actual bugfix here with the hardcoded entity property isn't tested but I'm not sure how to test it as we have no such entity type in core. Nor does DER use that, you would need an field type with a reference property not named entity.
Comment #62
jibranThanks for the review and a nice find.
How about something like
testInvalidSpecifier? It will test this change.
And it will throw an exception now instead of the fatal error.
Comment #63
berdirmissing docs :)
Comment #64
jibranThanks for the review @Berdir.
Comment #65
jax commentedWorks for me. Thanks.
Comment #68
berdirComment #69
jibranAdded #2818261: Add target_id as the main property just like ERItem for DER.
Comment #70
berdirOk, lets do this, to unblock entity query support for DER.
I don't think this has any effect on already working queries and for non-working queries, it might change them from a fatal error to an exception or a different exception, which is OK for 8.2.x.
Comment #71
xjmThe test failures look like #2806697: Random fail for AlreadyInstalledException and #2157927: Intermittent test fails in LocaleUpdateTest::testUpdateImportSourceRemote(), so requeuing tests on all environments.
Comment #72
xjmWell, technically, changing from one exception to a different one can disrupt code that handles the original exception. In this case it'd probably be okay. I'm confused though; isn't this adding support for new functionality? Which would mean 8.3.x and a CR needed.
Does EFQ document support for this already elsewhere?
(nit: ID should be uppercase, and there should be a comma before "i.e.").
Comment #73
jibranThank you @xjm for the review.
Yeah, it resolves the bug, changes the fatal into the exception, and adds new functionality.
Maybe we should commit the bug fix version of this patch to 8.2.x. I'm moving the issue to 8.3.x and I uploaded do not test patch for 8.2.x.
No, not yet I'll add the docs about this functionality once this is committed.
Thanks for pointing out a nit. New patch fixes it.
Comment #74
berdirChanging from one exception to another could be problematic, but we are changing from a fatal error to an exception. For queries that are *definitely* invalid and do not and will not ever work. You can only "handle" fatal errors in PHP7 where they are throwable/catchable error objects.
The new test shows an example of such a query, it is doing a condition with a typed data reference property linke the language on the LanguageItem field. That's a computed reference to the language object, not something that is persisted and queryable (there are configurable languages but this data type is not an entity and we don't support querying config entity properties that are referenced either).
IMHO, that change is OK, also as a patch release. But if you prefer to only have it in 8.3, fine with, it's @jibran/Dynamic Entity Reference that will then be unable to support entity queries until 8.3.0 is out.
Comment #75
berdirTo summarize and explain the "change". To be able to support the use case from DER, we need to refactor the code to support a way of specifying the target entity type (some_field.entity:node.type instead of just some_field.entity.type).
We are adding better validation for the detected/specified target entity type, and this is now also throwing an exception on entity queries that before didn't.. but then died with a fatal error.
And yes, it can be argued if this is a bugfix or a task or even new feature to support that kind of syntax. And it's probably a bit of everything. DER right now does not support entity queries and that is a bug. To be able to fix that, we need to ad this new feature.
Comment #76
catchSent this for a retest because all but one environments failed like this: https://www.drupal.org/pift-ci-job/523337
Doesn't seem related to the patch at all, but I haven't seen that elsewhere.
Switching from a fatal to an exception seems fine for a patch release to me as well, didn't fully review the patch though otherwise yet.
Comment #77
xjmThanks all for clarifying. That makes sense to me. If it's definitely a fatal in HEAD and not a different exception, I'm also fine with that change for a patch release as well and the only borderline bit for backporting is whether this is an "addition" or otherwise risky.
We should add the docs in the commit; it's the documentation gate. :) Also still need that CR pretty please since it's a bugish taskish featurish thing. A CR will also make me more comfortable with considering this for 8.2.x.
Comment #78
xjmOh regarding https://www.drupal.org/pift-ci-job/523337. That test ran during the hour where the timezone that hosts d.o was in the middle of the DST time change and the fail was related to datetime functionality. Hmmm. Could be a bug in HEAD for that special case.
Comment #79
xjmPosted #2825845: DST-related test failures in FilterDateTimeTest for the test fail.
Comment #80
jibranThanks! @Berdir for convincing the core committers. Thank you @catch and @xjm for the reviews.
Created Entity query allows to specify entity type ID for reference fields and added some docs to the patch. Moving back to 8.2.x as per #76 and #77.
I thought you meant docs on d.o i.e. https://www.drupal.org/docs/7/creating-custom-modules/howtos/how-to-use-.... :) Of course I don't want to jump the gate ;-)
PS: Please, add @Berdir to commit credit for all the discussions and help at DrupalCon Dublin. Can we also add the guy with 17 comments in this issue to commit credit? He came up with this idea and helped me a lot with this patch.
Comment #81
berdirI'd keep the . to end the sentence and start a new one, something like this:
Additionally, the target entity type can be specified by appending ":target_entity_type_id" to "entity".
Also, the second thing that we are fixing is that it does not have to be called entity. So the docs are not 100% correct on that.
We could start with something like "The column can be the reference property (usually "entity") for reference fields..
Comment #82
jibranThanks for the suggestions @Berdir. Fixed the #81.
Comment #83
berdirLooks good to me. lets see if that works for @xjm too :)
Comment #85
jibranComment #87
brunodboTestbot failure, resetting status due to https://www.drupal.org/node/2828045
Comment #88
jibranComment #90
jibranComment #92
jibranComment #95
catchCommitted/pushed to 8.3.x and cherry-picked to 8.2.x. Thanks!