git.sr.ht/~sircmpwn/gqlgen@v0.0.0-20200522192042-c84d29a1c940/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 *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  						i++
   134  					}
   135  
   136  					return users, nil
   137  				},
   138  			},
   139  		})
   140  		r = r.WithContext(ctx)
   141  		next.ServeHTTP(w, r)
   142  	})
   143  }
   144  
   145  func For(ctx context.Context) *Loaders {
   146  	return ctx.Value(loadersKey).(*Loaders)
   147  }
   148  
   149  ```
   150  
   151  This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call
   152  the fetch function. This function is a little ugly, but half of it is just building the SQL!
   153  
   154  Now lets update our resolver to call the dataloader:
   155  ```go
   156  func (r *todoResolver) UserLoader(ctx context.Context, obj *model.Todo) (*model.User, error) {
   157  	return dataloader.For(ctx).UserById.Load(obj.UserID)
   158  }
   159  ```
   160  
   161  The end result? just 2 queries!
   162  ```sql
   163  SELECT id, todo, user_id FROM todo
   164  SELECT id, name from user WHERE id IN (?,?,?,?,?)
   165  ```
   166  
   167  The generated UserLoader has a few other useful methods on it:
   168  
   169   - `LoadAll(keys)`: If you know up front you want a bunch users
   170   - `Prime(key, user)`: Used to sync state between similar loaders (usersById, usersByNote)
   171  
   172  You can see the full working example [here](https://github.com/vektah/gqlgen-tutorials/tree/master/dataloader).