gqlgen go, reduce db calls by adding one resolver

Issue

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

Solution

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
}

Answered By – L. Gangemi

Answer Checked By – Willingham (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.