github.com/geneva/gqlgen@v0.17.7-0.20230801155730-7b9317164836/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 stmt, err := r.db.PrepareContext(ctx, "SELECT id, name FROM users WHERE id = ?") 22 if err != nil { 23 return nil, err 24 } 25 defer stmt.Close() 26 27 rows, err := stmt.QueryContext(ctx, obj.UserID) 28 if err != nil { 29 return nil, err 30 } 31 defer rows.Close() 32 33 if !rows.Next() { 34 return nil, rows.Err() 35 } 36 37 var user model.User 38 if err := rows.Scan(&user.ID, &user.Name); err != nil { 39 return nil, err 40 } 41 return &user, nil 42 } 43 44 45 ``` 46 47 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. 48 49 eg: 50 ```sql 51 SELECT id, todo, user_id FROM todo 52 SELECT id, name FROM users WHERE id = ? 53 SELECT id, name FROM users WHERE id = ? 54 SELECT id, name FROM users WHERE id = ? 55 SELECT id, name FROM users WHERE id = ? 56 SELECT id, name FROM users WHERE id = ? 57 SELECT id, name FROM users WHERE id = ? 58 ``` 59 60 Whats even worse? most of those todos are all owned by the same user! We can do better than this. 61 62 ## Dataloader 63 64 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. 65 66 We're going to use [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader) to implement a dataloader for bulk-fetching users. 67 68 ```bash 69 go get -u github.com/graph-gophers/dataloader/v7 70 ``` 71 72 Next, we implement a data loader and a middleware for injecting the data loader on a request context. 73 74 ```go 75 package loaders 76 77 // import graph gophers with your other imports 78 import ( 79 "context" 80 "database/sql" 81 "net/http" 82 "strings" 83 "time" 84 85 "github.com/graph-gophers/dataloader/v7" 86 ) 87 88 type ctxKey string 89 90 const ( 91 loadersKey = ctxKey("dataloaders") 92 ) 93 94 // userReader reads Users from a database 95 type userReader struct { 96 db *sql.DB 97 } 98 99 // getUsers implements a batch function that can retrieve many users by ID, 100 // for use in a dataloader 101 func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] { 102 stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIds)-1)+`)`) 103 if err != nil { 104 return handleError[*model.User](len(userIds), err) 105 } 106 defer stmt.Close() 107 108 rows, err := stmt.QueryContext(ctx, userIds) 109 if err != nil { 110 return handleError[*model.User](len(userIds), err) 111 } 112 defer rows.Close() 113 114 result := make([]*dataloader.Result[*model.User], 0, len(userIds)) 115 for rows.Next() { 116 var user model.User 117 if err := rows.Scan(&user.ID, &user.Name); err != nil { 118 result = append(result, &dataloader.Result[*model.User]{Error: err}) 119 continue 120 } 121 result = append(result, &dataloader.Result[*model.User]{Data: &user}) 122 } 123 return result 124 } 125 126 // handleError creates array of result with the same error repeated for as many items requested 127 func handleError[T any](itemsLength int, err error) []*dataloader.Result[T] { 128 result := make([]*dataloader.Result[T], itemsLength) 129 for i := 0; i < itemsLength; i++ { 130 result[i] = &dataloader.Result[T]{Error: err} 131 } 132 return result 133 } 134 135 // Loaders wrap your data loaders to inject via middleware 136 type Loaders struct { 137 UserLoader *dataloader.Loader[string, *model.User] 138 } 139 140 // NewLoaders instantiates data loaders for the middleware 141 func NewLoaders(conn *sql.DB) *Loaders { 142 // define the data loader 143 ur := &userReader{db: conn} 144 return &Loaders{ 145 UserLoader: dataloader.NewBatchedLoader(ur.getUsers, dataloader.WithWait[string, *model.User](time.Millisecond)), 146 } 147 } 148 149 // Middleware injects data loaders into the context 150 func Middleware(loaders *Loaders, next http.Handler) http.Handler { 151 // return a middleware that injects the loader to the request context 152 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 r = r.WithContext(context.WithValue(r.Context(), loadersKey, loaders)) 154 next.ServeHTTP(w, r) 155 }) 156 } 157 158 // For returns the dataloader for a given context 159 func For(ctx context.Context) *Loaders { 160 return ctx.Value(loadersKey).(*Loaders) 161 } 162 163 // GetUser returns single user by id efficiently 164 func GetUser(ctx context.Context, userID string) (*model.User, error) { 165 loaders := For(ctx) 166 return loaders.UserLoader.Load(ctx, userID)() 167 } 168 169 // GetUsers returns many users by ids efficiently 170 func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) { 171 loaders := For(ctx) 172 return loaders.UserLoader.LoadMany(ctx, userIDs)() 173 } 174 175 ``` 176 177 Add the dataloader middleware to your server... 178 ```go 179 // create the query handler 180 var srv http.Handler = handler.NewDefaultServer(generated.NewExecutableSchema(...)) 181 // wrap the query handler with middleware to inject dataloader in requests. 182 // pass in your dataloader dependencies, in this case the db connection. 183 srv = loaders.Middleware(db, srv) 184 // register the wrapped handler 185 http.Handle("/query", srv) 186 ``` 187 188 Now lets update our resolver to call the dataloader: 189 ```go 190 func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { 191 return loaders.GetUser(ctx, obj.UserID) 192 } 193 ``` 194 195 The end result? Just 2 queries! 196 ```sql 197 SELECT id, todo, user_id FROM todo 198 SELECT id, name from user WHERE id IN (?,?,?,?,?) 199 ``` 200 201 You can see an end-to-end example [here](https://github.com/zenyui/gqlgen-dataloader).