-
Notifications
You must be signed in to change notification settings - Fork 78
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
How can i prevent multiple queries to the database when fetching foreign keys? #20
Comments
Are you sharing the loaders between resolvers? Or does each |
I send the loader with the request context like this:
And in the query resolver i use it like this:
|
So this is basically the same as this issue: graph-gophers/graphql-go#17. Dataloader is recommended there, so surely it is just something wrong with my implementation? |
Can you share your code for the batchFn? It sounds like the issue is there. It's probably a result of bad documentation on my end. |
I tried to implement it like @tonyghita proposed in #19
|
That batchFunc code looks correct to me. The default batch window is 6ms. Is it possible that your resolvers are taking longer than that to resolve? You could configure the batch window to be something longer to see if that solves your issue? |
I can't really see why the resolver would take so long, there is really not much going on there as you can see in Meetups() and UserResolver above. I tried setting the batch window to a 1 sec like this:
The returned response from the server is a list with 20 meetups and their owner. There are only two users that are owners to all the meetups, hence only the two select statements from users. So the caching works in the way that it prevents the resolver function from querying the database for the same user again. But in my application there could easily be 20 meetups with 20 different owners and the application would then query the database one time for each distinct user. |
Update: i found that calling thunk() from the Owner() field in the resolver directly after Load() causes the load to send the query as stated above. But when i change the the Owner method to:
And changing the Meetups() query resolver method to:
The result is then:
However this is not exactly the result i want, because then the SELECT * FROM "users" statement would be run even though Owner in the Meetup is not requested. |
@nicksrandall any updates on this?
Prints: When calling the thunk the wait for the batching is not taken into account. |
I'm not sure that |
@RubenSchmidt the "thunk" (or future) calls will block until they are resolved. The load method returns a thunk and not the values so that we can defer work. To help explain, the following code will work as you expected it to. func main() {
// go-cache will automaticlly cleanup expired items on given diration
c := cache.New(time.Duration(15*time.Minute), time.Duration(15*time.Minute))
cache := &Cache{c}
loader := dataloader.NewBatchedLoader(batchFunc, dataloader.WithCache(cache))
// queue up
thunk1 := loader.Load("key1")
thunk2 := loader.Load("key2")
thunk3 := loader.Load("key3")
// wait for resolution
thunk1()
thunk2()
thunk3()
}
func batchFunc(keys []string) []*dataloader.Result {
var results []*dataloader.Result
fmt.Printf("got keys: %v \n", keys)
for _, key := range keys {
results = append(results, &dataloader.Result{key, nil})
}
return results
} The reason that you can immediately call the thunk in your resolvers is because they are each run in their own goroutine. So the following would also work as you expected: func main() {
// go-cache will automaticlly cleanup expired items on given diration
c := cache.New(time.Duration(15*time.Minute), time.Duration(15*time.Minute))
cache := &Cache{c}
loader := dataloader.NewBatchedLoader(batchFunc, dataloader.WithCache(cache))
// immediately call the future function from loader
go loader.Load("key1")()
go loader.Load("key2")()
go loader.Load("key3")()
// wait for goroutines to finish before exiting program.
time.Sleep(1 * time.Second)
}
func batchFunc(keys []string) []*dataloader.Result {
var results []*dataloader.Result
fmt.Printf("got keys: %v \n", keys)
for _, key := range keys {
results = append(results, &dataloader.Result{key, nil})
}
return results
} |
P.S. @tonyghita thanks for always jumping in and lending a hand in the issues. I sincerely appreciate it! |
Alright thanks for clarifying! After looking some more into it, I found that by getting the value from the context in the Owner field and not when creating the resolver from the Meetups query it works as expected:
Passing the resolvers in the context feels a little bit clunky tho and an example of a better way would be much appreciated :) Thanks for all the help and a great project! |
@RubenSchmidt I agree it's pretty clunky. Off-topic, but I've started organizing the resolvers to have constructors where their dependencies are checked. I also pull out a These allow for really minimal and clean resolver functions. type MeetupResolver struct {
id string
loader *dataloader.Loader
}
// NewMeetupResolver validates dependencies and returns a new instance of MeetupResolver.
func NewMeetupResolver(ctx context.Context, id string) (*MeetResolver, error) {
loader, found := ctx.Value("meetupLoader").(*dataloader.Loader)
if !found {
return nil, errors.New("unable to find meetup loader")
}
if id == "" {
return nil, errors.New("no meetup ID specified")
}
return &MeetupResolver{id, loader}, nil
}
func (r *MeetupResolver) load() (*model.Meetup, error) {
// we can have any kinds of necessary checks here
if r.loader == nil {
return nil, errors.New("missing meetup loader")
}
// kind of verbose, but makes code bulletproof and easy to debug
if r.id == "" {
return nil, errors.New("missing meetup key")
}
// use the loader we attached in the constructor
thunk := r.loader.Load(r.id)
data, err := thunk()
if err != nil {
return nil, err
}
meetup, ok := data.(model.Meetup)
if !ok {
return nil, errors.New("unable to convert response to Meetup")
}
return meetup, nil
}
func (r *MeetupResolver) Owner(ctx context.Context) (*UserResolver, error) {
meetup, err := r.load() // keep the logic simple for resolvers
if err != nil {
return nil, err
}
return NewUserResolver(ctx, meetup.OwnerID) // analogous to NewMeetupResolver()
} |
@tonyghita That looks cleaner! I'm going to do it that way for now! Again, thanks for all the help! I'm closing this now. |
Hi! I am trying to use this library with graphql-go but i am struggling to get the batching to work when doing list queries that reference foreign key objects. Here's an example schema of what i mean:
In the MeetupResolver i have this:
When doing a query to meetups and asking for the Owner of the objects it is still doing a single query to the database for each of the different owners, like this:
Is there a way to batch all the owner ids and then send the query to the database?
The text was updated successfully, but these errors were encountered: