github.com/maeglindeveloper/gqlgen@v0.13.1-0.20210413081235-57808b12a0a0/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 Have you noticed some GraphQL queries end can make hundreds of database 9 queries, often with mostly repeated data? Lets take a look why and how to 10 fix it. 11 12 ## Query Resolution 13 14 Imagine if you had a simple query like this: 15 16 ```graphql 17 query { todos { users { name } } } 18 ``` 19 20 and our todo.user resolver looks like this: 21 ```go 22 func (r *todoResolver) UserRaw(ctx context.Context, obj *model.Todo) (*model.User, error) { 23 res := db.LogAndQuery(r.Conn, "SELECT id, name FROM dataloader_example.user WHERE id = ?", obj.UserID) 24 defer res.Close() 25 26 if !res.Next() { 27 return nil, nil 28 } 29 var user model.User 30 if err := res.Scan(&user.ID, &user.Name); err != nil { 31 panic(err) 32 } 33 return &user, nil 34 } 35 ``` 36 37 **Note**: I'm going to use go's low level `sql.DB` here. All of this will 38 work with whatever your favourite ORM is. 39 40 The query executor will call the Query.Todos resolver which does a `select * from todo` and 41 return N todos. Then for each of the todos, concurrently, call the Todo_user resolver, 42 `SELECT from USER where id = todo.user_id`. 43 44 45 eg: 46 ```sql 47 SELECT id, todo, user_id FROM todo 48 SELECT id, name FROM user WHERE id = ? 49 SELECT id, name FROM user WHERE id = ? 50 SELECT id, name FROM user WHERE id = ? 51 SELECT id, name FROM user WHERE id = ? 52 SELECT id, name FROM user WHERE id = ? 53 SELECT id, name FROM user WHERE id = ? 54 SELECT id, name FROM user WHERE id = ? 55 SELECT id, name FROM user WHERE id = ? 56 SELECT id, name FROM user WHERE id = ? 57 SELECT id, name FROM user WHERE id = ? 58 SELECT id, name FROM user WHERE id = ? 59 SELECT id, name FROM user WHERE id = ? 60 SELECT id, name FROM user WHERE id = ? 61 SELECT id, name FROM user WHERE id = ? 62 SELECT id, name FROM user WHERE id = ? 63 SELECT id, name FROM user WHERE id = ? 64 SELECT id, name FROM user WHERE id = ? 65 SELECT id, name FROM user WHERE id = ? 66 SELECT id, name FROM user WHERE id = ? 67 SELECT id, name FROM user WHERE id = ? 68 ``` 69 70 Whats even worse? most of those todos are all owned by the same user! We can do better than this. 71 72 ## Dataloader 73 74 What we need is a way to group up all of those concurrent requests, take out any duplicates, and 75 store them in case they are needed later on in request. The dataloader is just that, a request-scoped 76 batching and caching solution popularised by [facebook](https://github.com/facebook/dataloader). 77 78 We're going to use [dataloaden](https://github.com/vektah/dataloaden) to build our dataloaders. 79 In languages with generics, we could probably just create a DataLoader<User>, but golang 80 doesnt have generics. Instead we generate the code manually for our instance. 81 82 ```bash 83 go get github.com/vektah/dataloaden 84 mkdir dataloader 85 cd dataloader 86 go run github.com/vektah/dataloaden UserLoader int *gqlgen-tutorials/dataloader/graph/model.User 87 ``` 88 89 Next we need to create an instance of our new dataloader and tell how to fetch data. 90 Because dataloaders are request scoped, they are a good fit for `context`. 91 92 ```go 93 94 const loadersKey = "dataloaders" 95 96 type Loaders struct { 97 UserById UserLoader 98 } 99 100 func Middleware(conn *sql.DB, next http.Handler) http.Handler { 101 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 ctx := context.WithValue(r.Context(), loadersKey, &Loaders{ 103 UserById: UserLoader{ 104 maxBatch: 100, 105 wait: 1 * time.Millisecond, 106 fetch: func(ids []int) ([]*model.User, []error) { 107 placeholders := make([]string, len(ids)) 108 args := make([]interface{}, len(ids)) 109 for i := 0; i < len(ids); i++ { 110 placeholders[i] = "?" 111 args[i] = i 112 } 113 114 res := db.LogAndQuery(conn, 115 "SELECT id, name from dataloader_example.user WHERE id IN ("+strings.Join(placeholders, ",")+")", 116 args..., 117 ) 118 defer res.Close() 119 120 userById := map[int]*model.User{} 121 for res.Next() { 122 user := model.User{} 123 err := res.Scan(&user.ID, &user.Name) 124 if err != nil { 125 panic(err) 126 } 127 userById[user.ID] = &user 128 } 129 130 users := make([]*model.User, len(ids)) 131 for i, id := range ids { 132 users[i] = userById[id] 133 } 134 135 return users, nil 136 }, 137 }, 138 }) 139 r = r.WithContext(ctx) 140 next.ServeHTTP(w, r) 141 }) 142 } 143 144 func For(ctx context.Context) *Loaders { 145 return ctx.Value(loadersKey).(*Loaders) 146 } 147 148 ``` 149 150 This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call 151 the fetch function. This function is a little ugly, but half of it is just building the SQL! 152 153 Now lets update our resolver to call the dataloader: 154 ```go 155 func (r *todoResolver) UserLoader(ctx context.Context, obj *model.Todo) (*model.User, error) { 156 return dataloader.For(ctx).UserById.Load(obj.UserID) 157 } 158 ``` 159 160 The end result? just 2 queries! 161 ```sql 162 SELECT id, todo, user_id FROM todo 163 SELECT id, name from user WHERE id IN (?,?,?,?,?) 164 ``` 165 166 The generated UserLoader has a few other useful methods on it: 167 168 - `LoadAll(keys)`: If you know up front you want a bunch users 169 - `Prime(key, user)`: Used to sync state between similar loaders (usersById, usersByNote) 170 171 You can see the full working example [here](https://github.com/vektah/gqlgen-tutorials/tree/master/dataloader).