github.com/fortexxx/gqlgen@v0.10.3-0.20191216030626-ca5ea8b21ead/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] = ids[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(map[int]*User, len(ids)) 117 for res.Next() { 118 user := &User{} 119 err := res.Scan(&user.ID, &user.Name) 120 if err != nil { 121 panic(err) 122 } 123 users[user.ID] = user 124 } 125 126 output := make([]*User, len(ids)) 127 for i, id := range ids { 128 output[i] = users[id] 129 } 130 return output, nil 131 }, 132 } 133 ctx := context.WithValue(r.Context(), userLoaderKey, &userloader) 134 r = r.WithContext(ctx) 135 next.ServeHTTP(w, r) 136 }) 137 } 138 139 func (r *Resolver) Todo_userLoader(ctx context.Context, obj *Todo) (*User, error) { 140 return ctx.Value(userLoaderKey).(*UserLoader).Load(obj.UserID) 141 } 142 ``` 143 144 This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call 145 the fetch function. This function is a little ugly, but half of it is just building the SQL! 146 147 The end result? just 2 queries! 148 ```sql 149 SELECT id, todo, user_id FROM todo 150 SELECT id, name from user WHERE id IN (?,?,?,?,?) 151 ``` 152 153 The generated UserLoader has a few other useful methods on it: 154 155 - `LoadAll(keys)`: If you know up front you want a bunch users 156 - `Prime(key, user)`: Used to sync state between similar loaders (usersById, usersByNote) 157 158 You can see the full working example [here](https://github.com/vektah/gqlgen-tutorials/tree/master/dataloader).