gqlgen go, reduce db calls by adding one resolver

huangapple go评论117阅读模式
英文:

gqlgen go, reduce db calls by adding one resolver

问题

我正在遇到一些解决特定情况下性能降低的问题。我相信这是可以解决的,但我无法弄清楚如何做。

这是一个用于展示问题的示例模式:

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!]! 
}

我已经按照文档的说明进行了操作,特别是这部分 https://gqlgen.com/getting-started/#dont-eagerly-fetch-the-user
根据我的模式,它生成了以下解析器:

func (r *queryResolver) Answer(ctx context.Context, answerId string) (*models.Answer, error) {
...
#从数据库中检索单个 Answer 记录的单个查询
# Id 和文本填充 Answer 模型
#然后调用 Topic 解析器
...
}

func (r *queryResolver) Answers(ctx context.Context) ([]*models.Answer, error) {
...
#从数据库中检索 Answer 列表的单个查询
# Id 和文本填充 Answer 模型列表
-->#对于列表中的每个元素调用 Topic 解析器
...
}


func (r *answerResolver) Topic(ctx context.Context, obj *models.Answer) (*models.Topic, error) {
...
#从数据库中检索单个 Topic 记录的单个查询
#返回带有 idname  level  Topic 模型
...
}

当使用 answerId 参数调用 answer 查询时,将触发 answer 解析器,它解析 text 属性并调用 Topic 解析器。
Topic 解析器按预期工作,检索一个 Topic 并将其合并到 Answer 中并返回。

当没有使用 answerId 参数调用 answers 查询时,将触发 answers 解析器,它使用单个查询检索一个 answers 列表。
然后,对于该列表的每个元素,它调用 Topic 解析器。
Topic 解析器检索一个 Topic 并将其合并到单个 Answer 中并返回。

结果在两种情况下都是正确的,但是当我请求大量的 Answers 时,answers 查询会有性能问题。
对于每个 Answer,Topic 解析器都会被触发并执行一个查询来检索单个记录。

例如,如果我有2个 Answers --> 1个查询用于 [Answer0, Answer1],然后1个查询用于 Topic0 和1个查询用于 Topic1

例如,10个 Answers --> 1个查询用于 [Answer0, ..., Answer9],然后对于每个 TopicN,都会有10个查询

我希望能够获得一些类似于以下的 topics 数组解析器:

func (r *answersResolver) Topics(ctx context.Context, obj *[]models.Answer) (*[]models.Topic, error) {
...
#从数据库中检索 Topic 列表的单个查询
#返回带有 idname  level  Topic 模型列表
...
}

我期望返回的数组的每个元素都与相应的 Answers 数组的元素合并。

有没有可能以某种方式实现这个?在哪里可以找到这种方法的示例?
谢谢。

英文:

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

答案1

得分: 2

问题可以使用Dataloaders来解决。我必须为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
}

文档链接

英文:

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
}

huangapple
  • 本文由 发表于 2022年4月26日 21:15:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/72014642.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定