github.com/operandinc/gqlgen@v0.16.1/docs/content/reference/dataloaders.md (about) 1 --- 2 title: "Optimizing N+1 database queries using Dataloaders" 3 description: Speeding up your GraphQL requests by reducing the number of round trips to the database. 4 linkTitle: Dataloaders 5 menu: { main: { parent: 'reference', weight: 10 } } 6 --- 7 8 Dataloaders consolidate the retrieval of information into fewer, batched calls. This example demonstrates the value of dataloaders by consolidating many SQL queries into a single bulk query. 9 10 ## The Problem 11 12 Imagine your graph has query that lists todos... 13 14 ```graphql 15 query { todos { user { name } } } 16 ``` 17 18 and the `todo.user` resolver reads the `User` from a database... 19 ```go 20 func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { 21 res := db.LogAndQuery( 22 r.Conn, 23 "SELECT id, name FROM users WHERE id = ?", 24 obj.UserID, 25 ) 26 defer res.Close() 27 28 if !res.Next() { 29 return nil, nil 30 } 31 var user model.User 32 if err := res.Scan(&user.ID, &user.Name); err != nil { 33 panic(err) 34 } 35 return &user, nil 36 } 37 ``` 38 39 The query executor will call the `Query.Todos` resolver which does a `select * from todo` and returns `N` todos. If the nested `User` is selected, the above `UserRaw` resolver will run a separate query for each user, resulting in `N+1` database queries. 40 41 eg: 42 ```sql 43 SELECT id, todo, user_id FROM todo 44 SELECT id, name FROM users WHERE id = ? 45 SELECT id, name FROM users WHERE id = ? 46 SELECT id, name FROM users WHERE id = ? 47 SELECT id, name FROM users WHERE id = ? 48 SELECT id, name FROM users WHERE id = ? 49 SELECT id, name FROM users WHERE id = ? 50 ``` 51 52 Whats even worse? most of those todos are all owned by the same user! We can do better than this. 53 54 ## Dataloader 55 56 Dataloaders allow us to consolidate the fetching of `todo.user` across all resolvers for a given GraphQL request into a single database query and even cache the results for subsequent requests. 57 58 We're going to use [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader) to implement a dataloader for bulk-fetching users. 59 60 ```bash 61 go get -u github.com/graph-gophers/dataloader 62 ``` 63 64 Next, we implement a data loader and a middleware for injecting the data loader on a request context. 65 66 ```go 67 package storage 68 69 // import graph gophers with your other imports 70 import ( 71 "github.com/graph-gophers/dataloader" 72 ) 73 74 type ctxKey string 75 76 const ( 77 loadersKey = ctxKey("dataloaders") 78 ) 79 80 // UserReader reads Users from a database 81 type UserReader struct { 82 conn *sql.DB 83 } 84 85 // GetUsers implements a batch function that can retrieve many users by ID, 86 // for use in a dataloader 87 func (u *UserReader) GetUsers(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { 88 // read all requested users in a single query 89 userIDs := make([]string, len(keys)) 90 for ix, key := range keys { 91 userIDs[ix] = key.String() 92 } 93 res := u.db.Exec( 94 r.Conn, 95 "SELECT id, name 96 FROM users 97 WHERE id IN (?" + strings.Repeat(",?", len(userIDs-1)) + ")", 98 userIDs..., 99 ) 100 defer res.Close() 101 // return User records into a map by ID 102 userById := map[string]*model.User{} 103 for res.Next() { 104 user := model.User{} 105 if err := res.Scan(&user.ID, &user.Name); err != nil { 106 panic(err) 107 } 108 userById[user.ID] = &user 109 } 110 // return users in the same order requested 111 output := make([]*dataloader.Result, len(keys)) 112 for index, userKey := range keys { 113 user, ok := userById[userKey.String()] 114 if ok { 115 output[index] = &dataloader.Result{Data: record, Error: nil} 116 } else { 117 err := fmt.Errorf("user not found %s", userKey.String()) 118 output[index] = &dataloader.Result{Data: nil, Error: err} 119 } 120 } 121 return output 122 } 123 124 // Loaders wrap your data loaders to inject via middleware 125 type Loaders struct { 126 UserLoader *dataloader.Loader 127 } 128 129 // NewLoaders instantiates data loaders for the middleware 130 func NewLoaders(conn *sql.DB) *Loaders { 131 // define the data loader 132 userReader := &UserReader{conn: conn} 133 loaders := &Loaders{ 134 UserLoader: dataloader.NewBatchedLoader(u.GetUsers), 135 } 136 return loaders 137 } 138 139 // Middleware injects data loaders into the context 140 func Middleware(loaders *Loaders, next http.Handler) http.Handler { 141 // return a middleware that injects the loader to the request context 142 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 nextCtx := context.WithValue(r.Context(), loadersKey, loaders) 144 r = r.WithContext(nextCtx) 145 next.ServeHTTP(w, r) 146 }) 147 } 148 149 // For returns the dataloader for a given context 150 func For(ctx context.Context) *Loaders { 151 return ctx.Value(loadersKey).(*Loaders) 152 } 153 154 // GetUser wraps the User dataloader for efficient retrieval by user ID 155 func GetUser(ctx context.Context, userID string) (*model.User, error) { 156 loaders := For(ctx) 157 thunk := loaders.UserLoader.Load(ctx, dataloader.StringKey(userID)) 158 result, err := thunk() 159 if err != nil { 160 return nil, err 161 } 162 return result.(*model.User), nil 163 } 164 165 ``` 166 167 Now lets update our resolver to call the dataloader: 168 ```go 169 func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { 170 return storage.GetUser(ctx, obj.UserID) 171 } 172 ``` 173 174 The end result? Just 2 queries! 175 ```sql 176 SELECT id, todo, user_id FROM todo 177 SELECT id, name from user WHERE id IN (?,?,?,?,?) 178 ``` 179 180 You can see an end-to-end example [here](https://github.com/zenyui/gqlgen-dataloader).