github.com/vektah/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)