github.com/operandinc/gqlgen@v0.16.1/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  Dataloaders consolidate the retrieval of information into fewer, batched calls. This example demonstrates the value of dataloaders by consolidating many SQL queries into a single bulk query.
     9  
    10  ## The Problem
    11  
    12  Imagine your graph has query that lists todos...
    13  
    14  ```graphql
    15  query { todos { user { name } } }
    16  ```
    17  
    18  and the `todo.user` resolver reads the `User` from a database...
    19  ```go
    20  func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    21  	res := db.LogAndQuery(
    22  		r.Conn,
    23  		"SELECT id, name FROM users WHERE id = ?",
    24  		obj.UserID,
    25  	)
    26  	defer res.Close()
    27  
    28  	if !res.Next() {
    29  		return nil, nil
    30  	}
    31  	var user model.User
    32  	if err := res.Scan(&user.ID, &user.Name); err != nil {
    33  		panic(err)
    34  	}
    35  	return &user, nil
    36  }
    37  ```
    38  
    39  The query executor will call the `Query.Todos` resolver which does a `select * from todo` and returns `N` todos. If the nested `User` is selected, the above `UserRaw` resolver will run a separate query for each user, resulting in `N+1` database queries.
    40  
    41  eg:
    42  ```sql
    43  SELECT id, todo, user_id FROM todo
    44  SELECT id, name FROM users WHERE id = ?
    45  SELECT id, name FROM users WHERE id = ?
    46  SELECT id, name FROM users WHERE id = ?
    47  SELECT id, name FROM users WHERE id = ?
    48  SELECT id, name FROM users WHERE id = ?
    49  SELECT id, name FROM users WHERE id = ?
    50  ```
    51  
    52  Whats even worse? most of those todos are all owned by the same user! We can do better than this.
    53  
    54  ## Dataloader
    55  
    56  Dataloaders allow us to consolidate the fetching of `todo.user` across all resolvers for a given GraphQL request into a single database query and even cache the results for subsequent requests.
    57  
    58  We're going to use [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader) to implement a dataloader for bulk-fetching users.
    59  
    60  ```bash
    61  go get -u github.com/graph-gophers/dataloader
    62  ```
    63  
    64  Next, we implement a data loader and a middleware for injecting the data loader on a request context.
    65  
    66  ```go
    67  package storage
    68  
    69  // import graph gophers with your other imports
    70  import (
    71  	"github.com/graph-gophers/dataloader"
    72  )
    73  
    74  type ctxKey string
    75  
    76  const (
    77  	loadersKey = ctxKey("dataloaders")
    78  )
    79  
    80  // UserReader reads Users from a database
    81  type UserReader struct {
    82  	conn *sql.DB
    83  }
    84  
    85  // GetUsers implements a batch function that can retrieve many users by ID,
    86  // for use in a dataloader
    87  func (u *UserReader) GetUsers(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
    88  	// read all requested users in a single query
    89  	userIDs := make([]string, len(keys))
    90  	for ix, key := range keys {
    91  		userIDs[ix] = key.String()
    92  	}
    93  	res := u.db.Exec(
    94  		r.Conn,
    95  		"SELECT id, name
    96  		FROM users
    97  		WHERE id IN (?" + strings.Repeat(",?", len(userIDs-1)) + ")",
    98  		userIDs...,
    99  	)
   100  	defer res.Close()
   101  	// return User records into a map by ID
   102  	userById := map[string]*model.User{}
   103  	for res.Next() {
   104  		user := model.User{}
   105  		if err := res.Scan(&user.ID, &user.Name); err != nil {
   106  			panic(err)
   107  		}
   108  		userById[user.ID] = &user
   109  	}
   110  	// return users in the same order requested
   111  	output := make([]*dataloader.Result, len(keys))
   112  	for index, userKey := range keys {
   113  		user, ok := userById[userKey.String()]
   114  		if ok {
   115  			output[index] = &dataloader.Result{Data: record, Error: nil}
   116  		} else {
   117  			err := fmt.Errorf("user not found %s", userKey.String())
   118  			output[index] = &dataloader.Result{Data: nil, Error: err}
   119  		}
   120  	}
   121  	return output
   122  }
   123  
   124  // Loaders wrap your data loaders to inject via middleware
   125  type Loaders struct {
   126  	UserLoader *dataloader.Loader
   127  }
   128  
   129  // NewLoaders instantiates data loaders for the middleware
   130  func NewLoaders(conn *sql.DB) *Loaders {
   131  	// define the data loader
   132  	userReader := &UserReader{conn: conn}
   133  	loaders := &Loaders{
   134  		UserLoader: dataloader.NewBatchedLoader(u.GetUsers),
   135  	}
   136  	return loaders
   137  }
   138  
   139  // Middleware injects data loaders into the context
   140  func Middleware(loaders *Loaders, next http.Handler) http.Handler {
   141  	// return a middleware that injects the loader to the request context
   142  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   143  		nextCtx := context.WithValue(r.Context(), loadersKey, loaders)
   144  		r = r.WithContext(nextCtx)
   145  		next.ServeHTTP(w, r)
   146  	})
   147  }
   148  
   149  // For returns the dataloader for a given context
   150  func For(ctx context.Context) *Loaders {
   151  	return ctx.Value(loadersKey).(*Loaders)
   152  }
   153  
   154  // GetUser wraps the User dataloader for efficient retrieval by user ID
   155  func GetUser(ctx context.Context, userID string) (*model.User, error) {
   156  	loaders := For(ctx)
   157  	thunk := loaders.UserLoader.Load(ctx, dataloader.StringKey(userID))
   158  	result, err := thunk()
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	return result.(*model.User), nil
   163  }
   164  
   165  ```
   166  
   167  Now lets update our resolver to call the dataloader:
   168  ```go
   169  func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
   170  	return storage.GetUser(ctx, obj.UserID)
   171  }
   172  ```
   173  
   174  The end result? Just 2 queries!
   175  ```sql
   176  SELECT id, todo, user_id FROM todo
   177  SELECT id, name from user WHERE id IN (?,?,?,?,?)
   178  ```
   179  
   180  You can see an end-to-end example [here](https://github.com/zenyui/gqlgen-dataloader).