I'm having some trouble resolving a specific situation which results in performances reduction. I'm quite sure that is something which can be done, but I can't figure oute how to do it.
Here's an example schema for exposing the problem:
type Answer{
answerId: String!
text: String!
topic: Topic!
}
type Topic {
topicId: String!
name: String!
level: Int!
}
extend type Query {
answer(answerId: String!): Answer!
answers: [Answer!]!
}
I've followed the documentation, expecially this part https://gqlgen.com/getting-started/#dont-eagerly-fetch-the-user From my schema, It generates the following resolvers:
func (r *queryResolver) Answer(ctx context.Context, answerId string) (*models.Answer, error) {
...
#Single Query which retrives single record of Answer from DB.
#Fills a model Answer with the Id and the text
#Proceeds by calling the Topic resolver
...
}
func (r *queryResolver) Answers(ctx context.Context) ([]*models.Answer, error) {
...
#Single Query which retrives list of Answers from DB
#Fills a list of model Answer with the Id and the text
-->#For each element of that list, it calls the Topic resolver
...
}
func (r *answerResolver) Topic(ctx context.Context, obj *models.Answer) (*models.Topic, error) {
...
#Single Query which retrives single record of Topic from DB
#Return a model Topic with id, name and level
...
}
When the answer
query gets called with answerId
parameter, the answer
resolvers gets triggered, it resolves the text
property and calls the Topic
resolver.
The Topic
resolver works as expected, retrives a Topic
it merges it inside the Answer
and return.
When the answers
query gets called without answerId
parameter, the answer
resolvers gets triggered, it retrives a list of answers
with a single query.
Then, for each element of that list , it calls the Topic
resolver.
The Topic
retrives a Topic
and it merges it inside the single Answer
and return.
The results it's ok in both cases, but the answers
query as a performance problem if I'm asking for a lot of Answers.
For each of the answer, the Topic
resolver gets triggered and performs a query to retrive a single record.
Ex. If I've 2 Answers --> 1 Query for [Answer0, Answer1]
, then 1 Query for Topic0
and 1 for Topic1
Ex. 10 Answers --> 1 for [Answer0, ..., Answer9]
and then 10 for each TopicN
I would like to obtain some topic
array resolver like
func (r *answersResolver) Topics(ctx context.Context, obj *[]models.Answer) (*[]models.Topic, error) {
...
#Single Query which retrives list of Topics from DB
#Return a list of model Topic with id, name and level
...
}
And I expect every element of the returned array to merge with the corresponding element of the Answers
array.
Is it possible in some way? Where I can find an example of such approach? Thanks
CodePudding user response:
The problem could be solved using Dataloaders (docs)
I had to implement the following datasource for Topics
:
package dataloader
import (
"github.com/graph-gophers/dataloader"
)
type ctxKey string
const (
loadersKey = ctxKey("dataloaders")
)
type TopicReader struct {
conn *sql.DB
}
func (t *TopicReader) GetTopics(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
topicIDs := make([]string, len(keys))
for ix, key := range keys {
topicIDs[ix] = key.String()
}
res := u.db.Exec(
r.Conn,
"SELECT id, name, level
FROM topics
WHERE id IN (?" strings.Repeat(",?", len(topicIDs-1)) ")",
topicIDs...,
)
defer res.Close()
output := make([]*dataloader.Result, len(keys))
for index, _ := range keys {
output[index] = &dataloader.Result{Data: res[index], Error: nil}
}
return output
}
type Loaders struct {
TopicLoader *dataloader.Loader
}
func NewLoaders(conn *sql.DB) *Loaders {
topicReader := &TopicReader{conn: conn}
loaders := &Loaders{
TopicLoader: dataloader.NewBatchedLoader(t.GetTopics),
}
return loaders
}
func Middleware(loaders *Loaders, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCtx := context.WithValue(r.Context(), loadersKey, loaders)
r = r.WithContext(nextCtx)
next.ServeHTTP(w, r)
})
}
func For(ctx context.Context) *Loaders {
return ctx.Value(loadersKey).(*Loaders)
}
func GetTopic(ctx context.Context, topicID string) (*model.Topic, error) {
loaders := For(ctx)
thunk := loaders.TopicLoader.Load(ctx, dataloader.StringKey(topicID))
result, err := thunk()
if err != nil {
return nil, err
}
return result.(*model.Topic), nil
}