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