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).