github.com/99designs/gqlgen@v0.17.45/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 [vikstrous/dataloadgen](https://github.com/vikstrous/dataloadgen) to implement a dataloader for bulk-fetching users. 67 68 ```bash 69 go get github.com/vikstrous/dataloadgen 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 vikstrous/dataloadgen with your other imports 78 import ( 79 "context" 80 "database/sql" 81 "net/http" 82 "strings" 83 "time" 84 85 "github.com/vikstrous/dataloadgen" 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) ([]*model.User, []error) { 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 nil, []error{err} 105 } 106 defer stmt.Close() 107 108 rows, err := stmt.QueryContext(ctx, userIDs) 109 if err != nil { 110 return nil, []error{err} 111 } 112 defer rows.Close() 113 114 users := make([]*model.User, 0, len(userIDs)) 115 errs := make([]error, 0, len(userIDs)) 116 for rows.Next() { 117 var user model.User 118 if err := rows.Scan(&user.ID, &user.Name); err != nil { 119 errs = append(errs, err) 120 continue 121 } 122 users = append(users, &user) 123 } 124 return users, errs 125 } 126 127 // Loaders wrap your data loaders to inject via middleware 128 type Loaders struct { 129 UserLoader *dataloadgen.Loader[string, *model.User] 130 } 131 132 // NewLoaders instantiates data loaders for the middleware 133 func NewLoaders(conn *sql.DB) *Loaders { 134 // define the data loader 135 ur := &userReader{db: conn} 136 return &Loaders{ 137 UserLoader: dataloadgen.NewLoader(ur.getUsers, dataloadgen.WithWait(time.Millisecond)), 138 } 139 } 140 141 // Middleware injects data loaders into the context 142 func Middleware(conn *sql.DB, next http.Handler) http.Handler { 143 // return a middleware that injects the loader to the request context 144 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 loader := NewLoaders(conn) 146 r = r.WithContext(context.WithValue(r.Context(), loadersKey, loader)) 147 next.ServeHTTP(w, r) 148 }) 149 } 150 151 // For returns the dataloader for a given context 152 func For(ctx context.Context) *Loaders { 153 return ctx.Value(loadersKey).(*Loaders) 154 } 155 156 // GetUser returns single user by id efficiently 157 func GetUser(ctx context.Context, userID string) (*model.User, error) { 158 loaders := For(ctx) 159 return loaders.UserLoader.Load(ctx, userID) 160 } 161 162 // GetUsers returns many users by ids efficiently 163 func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, error) { 164 loaders := For(ctx) 165 return loaders.UserLoader.LoadAll(ctx, userIDs) 166 } 167 168 ``` 169 170 Add the dataloader middleware to your server... 171 ```go 172 // create the query handler 173 var srv http.Handler = handler.NewDefaultServer(generated.NewExecutableSchema(...)) 174 // wrap the query handler with middleware to inject dataloader in requests. 175 // pass in your dataloader dependencies, in this case the db connection. 176 srv = loaders.Middleware(db, srv) 177 // register the wrapped handler 178 http.Handle("/query", srv) 179 ``` 180 181 Now lets update our resolver to call the dataloader: 182 ```go 183 func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { 184 return loaders.GetUser(ctx, obj.UserID) 185 } 186 ``` 187 188 The end result? Just 2 queries! 189 ```sql 190 SELECT id, todo, user_id FROM todo 191 SELECT id, name from user WHERE id IN (?,?,?,?,?) 192 ``` 193 194 You can see an end-to-end example [here](https://github.com/vikstrous/dataloadgen-example).