-
Notifications
You must be signed in to change notification settings - Fork 493
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
Expose the tree of field subselections to the resolver on demand #169
base: master
Are you sure you want to change the base?
Conversation
If I'm interpreting semaphoreci setup errors correctly, someone needs to fix semaphoreci-github integration for this to build to pass? |
SemaphoreCI hasn't really handled the I'll look into what I can do to straighten it out. |
It looks like SemaphoreCI issue is fixed. Anything else that is stopping us from merging this? |
I see no reasons for this not to be merged (naturally, as I filed this PR :) ); I use this patch for some time in one of private projects |
Copied from graph-gophers#169 and used package 'query' instead of 'selected'.
@xmirya I applied this patch to our fork along with a basic test: https://github.com/qdentity/graphql-go/blob/5472fce1344b4de8067df6cf53d09384c6533ff3/graphql_test.go |
@xmirya Added support for wrapper types, i.e., can be any |
Hey, Thanks for the PR. I also came up with a solution but I like your solution better. I have a couple of suggestions:
with GraphQL Query
would benefit from a selection set having variable values for each field. When you build the SQL/Graph query for db from the selection set, you could identify the pagination values for the field Looking forward to your reply, thanks! |
Hey, thanks for the contribution! I'm still getting up to speed with the implementation details of execution but I'd like to get this feature in. I believe we have a similar PR in #70. It'd be nice to include some unit tests to cover the new behavior, and perhaps a benchmark to get an idea of how much additional overhead the feature adds (if any). |
@salmana1 For including it into the context: i'd not do it for three reasons: (1) the optional parameter allows constructing the tree only when the method requests it by specifying the optional parameter; with the context solution we'd have to either always construct it or pass some factory function that constructs it (which is a bit ugly IMO) (2) the optional parameter solution ensures the runtime and parsing performance for existing code does not change (3) IMO passing whatever as a context parameter under some key is not "backward compatible", whatever key name is chosen, it still might collide with someone using it in the existing code. For variables in subselections - will add it to the patch. @tonyghita - will add tests (prob. would be nice to provide an example/update docs as well); for benchmarking - i expect it not to change the runtime performance of the existing 1- or 2-parameter methods - the difference is one "if" branch; same about memory use - Go booleans are packed AFAIK; but might be useful to show how adding this optional parameter affects the GQL method call speed. |
Thanks for replying back and clarifying. After commenting here, I thought about the context approach again and came up with same conclusion as yourself. I am liking the optional argument approach more now. The patch with variables would be nice. If you haven't started working on the patch yet, I can create a PR. It would be awesome if you could please merge this PR soon. I think we can follow up with another PR to add unit tests and benchmark results. |
I left some comments on this subject here: #70 (comment) As I mentioned there, I believe that the context is the idiomatic place to put these fields even if it requires a constructor function. We could also used a typed key for the context variable so that there is no possible collision. That said, I don't feel so strongly about this as to prevent this PR from being merged if the general consensus is that an extra argument is preferred to using the context. |
I think people will use selection set more often than not. Perhaps we could achieve best of both worlds by introducing a configurable option via SchemaOpt. Construct the selection set tree and put it in the context only when it is configured. This approach will make sure that there is no performance impact to existing code and there is an idiomatic solution. What do you guys think? |
I haven't experienced a need for the selection set in resolvers, but I can see how this may be important if your resolvers directly communicated with a database. Is anyone able to provide an example of how they would use this functionality in their resolver code? One or more concrete code examples could help guide this discussion. |
My situation (and I know I'm just repeating what's already been mentioned
here) : I have semi complex relation between user (owner) and a couple of
other entity types (campaigns, assets, content) + spme relation between
these entities. All this is stored in the rdbms. Atm I have two options: 1)
preload all relations down to Nth level or 2) load relations (query the db)
of each node.
None of those 2 is optimal. If I would be able to get the (whole tree) of
selected fields at least in the 1st level resolver ( but preferably in any
resolver) I could make a smart preloader of relations that would load just
enough.
I have already everything in place, all I need now is this tree of
selectors :)
I would prefer (just a personal preference) it is passed by argument
directly to the resolver func (optional if detected by reflection).
|
We are using DGraph as a database and having the selection set helps us convert a GraphQL query into a DGraph query. Or at least know which fields/nodes we need to pull from the DB. We do not want to pull all the nodes & reverse nodes and then figure out what to send back to client. May I ask, how are you interacting with db? Are you pulling everything from DB and filtering what to send back to client in the app? |
@salmana1 We don't interact with any databases directly—instead we make service requests to hundreds of data services which front databases. The databases are denormalized (generally joins happen in the application layer). We haven't really had to care if we are over-fetching from services since it all happens within the same network, so generally the costs of over-fetching at this level are neglible. This is the approach I've tried to demonstrate in https://github.com/tonyghita/graphql-go-example |
Hi, @tonyghita My use case (currently we are using apollo-server with nodejs, providing the possibility to get tree field on the resolver level). The (simplified) schema :
Screens :
When an user select a program the app displays a program page with all the associated videos.
Resolvers : What we have done with apollo-graphql : Using a dataloader and count request in the first case is more efficient. It's why we need to have access to the tree of field to achieve these kinds of optimization. I hope my use case will help you ! |
So, where are we with this? :) Planning to push my API in (pre)production mid-May and I would really like to have this support built in.. ty. |
Any update? |
+1 |
Any update? I rly need this feature |
Any update? This is indeed a make-or-break feature for me as well- I can't get any semblance of good performance without this. |
Just ran into this issue ourselves, found it strange that it wasn't already in the lib. But great that it's close to merge, ship it 🚀 :) |
This is an important and repeatedly requested feature. Is there anything specifically preventing this PR from being merged? Anything we can help? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add unit tests for the new functionality? At least one resolver and then make sure that the proper fields get passed as arguments?
@@ -9,6 +9,7 @@ import ( | |||
"github.com/graph-gophers/graphql-go/internal/common" | |||
"github.com/graph-gophers/graphql-go/internal/exec/packer" | |||
"github.com/graph-gophers/graphql-go/internal/schema" | |||
pubselected "github.com/graph-gophers/graphql-go/selected" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the pub
prefix stand for?
|
||
- Optional `context.Context` argument. | ||
- Mandatory `*struct { ... }` argument if the corresponding GraphQL field has arguments. The names of the struct fields have to be [exported](https://golang.org/ref/spec#Exported_identifiers) and have to match the names of the GraphQL arguments in a non-case-sensitive way. | ||
- Optional `[]selected.SelectedField` argument to receive the tree of selected subfields in the GraphQL query (useful for preloading of database relations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the repetition of the work "selected" in: selected.SelectedField
. It'd be nice if we can avoid that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe selected.Field
🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, even better package name...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just came across this PR. Given the author has not replied, I'm happy to do these changes (along with tests) if there's any chance that this gets approved and merged afterwards cc/ @pavelnikolov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jesushernandez this is the most requested feature and I really want it to get some traction but I'm not sure that this is the best approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can I help getting more traction?
This would be so good for the library :) . Are there any news regarding this PR? |
👍 Definitely would like to see this get implemented, it would make queries from resolvers far more efficient in some use cases. Having it be optional will allow certain upstream resolvers still follow natural convention, but allow DB queries to be a lot faster. Note I think, should this get implemented, it would be highly worthwhile calling out certain traps one could get caught in by relying on this (especially if they want to use For example: query {
authors {
id
name
books {
id
name
relatedBooks {
id
name
isbn
}
}
}
} The major benefit to this query IMHO would not be the fact that I need not blindly But there'd be a small gotcha for someone who might naively try to over-optimize, such as the case where they granularly want to select (instead of the |
Is there any way to get this merged soon? |
What's up with DealTap#5 ? They've had that over there.. do we want something similar here? |
Bump. Any news? |
+100 for having access to selected fields and their arguments, especially for pagination. |
@tonyghita up, Any news? :) |
After catching up on all of the relevant conversation, it seems like this PR (#169) overlaps with two other viable PRs to provide access to the query from the resolver.
I've implemented a similar approach in my own fork. Similarly to the approach this PR, I compute the value returned by context lazily. There are two major differences between my fork and this PR:
These are the data structures I've settled on: type Field struct {
Alias string
Name string
TypeName string
Arguments map[string]interface{}
ArgumentTypes map[string]ArgumentType
Selected []Field
}
type ArgumentType struct {
NamedType string
Elem *ArgumentType
NonNull bool
}
type FieldFunc func() Field
func GetFieldFromContext(ctx context.Context) (field Field, ok bool) {
fieldFunc, ok := ctx.Value(FieldContextKey).(FieldFunc)
if ok {
field = fieldFunc()
}
return
} |
This has been one of the most commonly requested features for this library. I agree that we need to expose the selected fields, however, I'm still not completely sure if we need to expose the fields through the context or through a strongly typed argument in the resolver. And if we go the path of strongly typed argument in the resolver we need to make sure that this is opt-in and doesn't introduce any breaking changes to existing codebases. I agree that having a field with its subfields makes a lot of sense - especially for graph databases. |
I don't have a strong opinion about which approach is best. If we wanted to get things moving with something we might start with the context approach and mark the package as experimental. This way people can opt into it and experiment with it without needing the project to make a long term commitment about compatibility. Once the design settles and the preferred approach becomes more obvious, then we either remove the experimental tag or deprecate the package. |
You can even link to my own PR with is quite similar to others #422 I most admit I did not researched enough before doing my own work, so I apologize in advance :) We use it in production, our main current use case is to inspect Unions selected by the user so the backend registered to only a subset of the fields. |
Yes, your PR looks solid as well! Assuming they expose adequate information to forward the current resolution operation, I look forward to any of these being merged |
And so do I :) |
Any update? |
How can I help to get this closed? I'm happy to spend a couple hours on this. |
I manually integrated this PR into the newest code and it works like a charm. I agree with the two naming oddities (selected.selected and pubselected). There is a small incompatibility where the api-code calls it's own resolver-calls: the []selected.SelectedField argument must be supplied then also. In practice one will implement an extra function to do that. |
@jhelberg: Arguments throw a wrench in that, which is out of scope for this PR but is part of a derived changeset I'm using in production. (I intend to open a PR for that once this PR lands.) The selection data would have to be expressed as a map of slices to capture cases where a field is resolved more than once, e.g. query {
foo: node(id: "foo") { … }
bar: node(id: "bar") { … }
}
My resolvers usually look something like: var withFoo, withBar bool
for _, field := range fields {
switch field.Name {
case "foo":
withFoo = true
case "bar":
withBar = true
}
}
// do the operation, optionally withFoo or withBar When my resolver needs nested arguments, I fish them out while iterating: var nodeIds []string
for _, field := range fields {
switch field.Name {
case "node":
nodeIds = append(nodeIds, field.arguments["id"])
}
}
// bulk-retrieve nodeIds |
What's requested in #17 .
Use case: we have
Order
andOwner
entities stored in the DB, eachOrder
refers to oneOwner
. We want to expose the API to list orders, optionally providing owners info, e.g.W/o knowing whether the user has requested
Owner
info in theorders
resolver we end up with doing 1 query for the orders list + N queries for eachOwner
(assuming eachOrder
has a differentOwner
). Iforders
resolver might check ahead of time whetherOwner
is requested, it might end up running just one (in case of relational DB, usingINNER JOIN
) or two (in the common case, one to fetch the orders, second to fetch owners by the list of IDs found from the 1st query) queries.This is well aligned with how many ORMs allow related entity preloading, like
orm.GetOrders(...).With("Owner")
, soorder.GetOwner()
later does not result in a separate query.