Search code examples
gographqlgqlgen

gqlgen go, reduce db calls by adding one resolver


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
    }