英文:
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 记录的单个查询
#返回带有 id、name 和 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 列表的单个查询
#返回带有 id、name 和 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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论