github.com/bowd/gqlgen@v0.7.2/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' } } 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 *Resolver) Todo_user(ctx context.Context, obj *Todo) (*User, error) { 23 res := logAndQuery(r.db, "SELECT id, name FROM user WHERE id = ?", obj.UserID) 24 defer res.Close() 25 26 if !res.Next() { 27 return nil, nil 28 } 29 var user 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.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 85 dataloaden github.com/full/package/name.User 86 ``` 87 88 Next we need to create an instance of our new dataloader and tell how to fetch data. 89 Because dataloaders are request scoped, they are a good fit for `context`. 90 91 ```go 92 93 const userLoaderKey = "userloader" 94 95 func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { 96 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 userloader := UserLoader{ 98 maxBatch: 100, 99 wait: 1 * time.Millisecond, 100 fetch: func(ids []int) ([]*User, []error) { 101 placeholders := make([]string, len(ids)) 102 args := make([]interface{}, len(ids)) 103 for i := 0; i < len(ids); i++ { 104 placeholders[i] = "?" 105 args[i] = i 106 } 107 108 res := logAndQuery(db, 109 "SELECT id, name from user WHERE id IN ("+ 110 strings.Join(placeholders, ",")+")", 111 args..., 112 ) 113 114 defer res.Close() 115 116 users := make([]*User, len(ids)) 117 i := 0 118 for res.Next() { 119 users[i] = &User{} 120 err := res.Scan(&users[i].ID, &users[i].Name) 121 if err != nil { 122 panic(err) 123 } 124 i++ 125 } 126 127 return users, nil 128 }, 129 } 130 ctx := context.WithValue(r.Context(), userLoaderKey, &userloader) 131 r = r.WithContext(ctx) 132 next.ServeHTTP(w, r) 133 }) 134 } 135 136 func (r *Resolver) Todo_userLoader(ctx context.Context, obj *Todo) (*User, error) { 137 return ctx.Value(userLoaderKey).(*UserLoader).Load(obj.UserID) 138 } 139 ``` 140 141 This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call 142 the fetch function. This function is a little ugly, but half of it is just building the SQL! 143 144 The end result? just 2 queries! 145 ```sql 146 SELECT id, todo, user_id FROM todo 147 SELECT id, name from user WHERE id IN (?,?,?,?,?) 148 ``` 149 150 The generated UserLoader has a few other useful methods on it: 151 152 - `LoadAll(keys)`: If you know up front you want a bunch users 153 - `Prime(key, user)`: Used to sync state between similar loaders (usersById, usersByNote) 154 155 You can see the full working example [here](https://github.com/vektah/gqlgen-tutorials/tree/master/dataloader)