github.com/geneva/gqlgen@v0.17.7-0.20230801155730-7b9317164836/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  	stmt, err := r.db.PrepareContext(ctx, "SELECT id, name FROM users WHERE id = ?")
    22  	if err != nil {
    23  		return nil, err
    24  	}
    25  	defer stmt.Close()
    26  
    27  	rows, err := stmt.QueryContext(ctx, obj.UserID)
    28  	if err != nil {
    29  		return nil, err
    30  	}
    31  	defer rows.Close()
    32  
    33  	if !rows.Next() {
    34  		return nil, rows.Err()
    35  	}
    36  
    37  	var user model.User
    38  	if err := rows.Scan(&user.ID, &user.Name); err != nil {
    39  		return nil, err
    40  	}
    41  	return &user, nil
    42  }
    43  
    44  
    45  ```
    46  
    47  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.
    48  
    49  eg:
    50  ```sql
    51  SELECT id, todo, user_id FROM todo
    52  SELECT id, name FROM users WHERE id = ?
    53  SELECT id, name FROM users WHERE id = ?
    54  SELECT id, name FROM users WHERE id = ?
    55  SELECT id, name FROM users WHERE id = ?
    56  SELECT id, name FROM users WHERE id = ?
    57  SELECT id, name FROM users WHERE id = ?
    58  ```
    59  
    60  Whats even worse? most of those todos are all owned by the same user! We can do better than this.
    61  
    62  ## Dataloader
    63  
    64  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.
    65  
    66  We're going to use [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader) to implement a dataloader for bulk-fetching users.
    67  
    68  ```bash
    69  go get -u github.com/graph-gophers/dataloader/v7
    70  ```
    71  
    72  Next, we implement a data loader and a middleware for injecting the data loader on a request context.
    73  
    74  ```go
    75  package loaders
    76  
    77  // import graph gophers with your other imports
    78  import (
    79  	"context"
    80  	"database/sql"
    81  	"net/http"
    82  	"strings"
    83  	"time"
    84  
    85  	"github.com/graph-gophers/dataloader/v7"
    86  )
    87  
    88  type ctxKey string
    89  
    90  const (
    91  	loadersKey = ctxKey("dataloaders")
    92  )
    93  
    94  // userReader reads Users from a database
    95  type userReader struct {
    96  	db *sql.DB
    97  }
    98  
    99  // getUsers implements a batch function that can retrieve many users by ID,
   100  // for use in a dataloader
   101  func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] {
   102  	stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIds)-1)+`)`)
   103  	if err != nil {
   104  		return handleError[*model.User](len(userIds), err)
   105  	}
   106  	defer stmt.Close()
   107  
   108  	rows, err := stmt.QueryContext(ctx, userIds)
   109  	if err != nil {
   110  		return handleError[*model.User](len(userIds), err)
   111  	}
   112  	defer rows.Close()
   113  
   114  	result := make([]*dataloader.Result[*model.User], 0, len(userIds))
   115  	for rows.Next() {
   116  		var user model.User
   117  		if err := rows.Scan(&user.ID, &user.Name); err != nil {
   118  			result = append(result, &dataloader.Result[*model.User]{Error: err})
   119  			continue
   120  		}
   121  		result = append(result, &dataloader.Result[*model.User]{Data: &user})
   122  	}
   123  	return result
   124  }
   125  
   126  // handleError creates array of result with the same error repeated for as many items requested
   127  func handleError[T any](itemsLength int, err error) []*dataloader.Result[T] {
   128  	result := make([]*dataloader.Result[T], itemsLength)
   129  	for i := 0; i < itemsLength; i++ {
   130  		result[i] = &dataloader.Result[T]{Error: err}
   131  	}
   132  	return result
   133  }
   134  
   135  // Loaders wrap your data loaders to inject via middleware
   136  type Loaders struct {
   137  	UserLoader *dataloader.Loader[string, *model.User]
   138  }
   139  
   140  // NewLoaders instantiates data loaders for the middleware
   141  func NewLoaders(conn *sql.DB) *Loaders {
   142  	// define the data loader
   143  	ur := &userReader{db: conn}
   144  	return &Loaders{
   145  		UserLoader: dataloader.NewBatchedLoader(ur.getUsers, dataloader.WithWait[string, *model.User](time.Millisecond)),
   146  	}
   147  }
   148  
   149  // Middleware injects data loaders into the context
   150  func Middleware(loaders *Loaders, next http.Handler) http.Handler {
   151  	// return a middleware that injects the loader to the request context
   152  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   153  		r = r.WithContext(context.WithValue(r.Context(), loadersKey, loaders))
   154  		next.ServeHTTP(w, r)
   155  	})
   156  }
   157  
   158  // For returns the dataloader for a given context
   159  func For(ctx context.Context) *Loaders {
   160  	return ctx.Value(loadersKey).(*Loaders)
   161  }
   162  
   163  // GetUser returns single user by id efficiently
   164  func GetUser(ctx context.Context, userID string) (*model.User, error) {
   165  	loaders := For(ctx)
   166  	return loaders.UserLoader.Load(ctx, userID)()
   167  }
   168  
   169  // GetUsers returns many users by ids efficiently
   170  func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) {
   171  	loaders := For(ctx)
   172  	return loaders.UserLoader.LoadMany(ctx, userIDs)()
   173  }
   174  
   175  ```
   176  
   177  Add the dataloader middleware to your server...
   178  ```go
   179  // create the query handler
   180  var srv http.Handler = handler.NewDefaultServer(generated.NewExecutableSchema(...))
   181  // wrap the query handler with middleware to inject dataloader in requests.
   182  // pass in your dataloader dependencies, in this case the db connection.
   183  srv = loaders.Middleware(db, srv)
   184  // register the wrapped handler
   185  http.Handle("/query", srv)
   186  ```
   187  
   188  Now lets update our resolver to call the dataloader:
   189  ```go
   190  func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
   191  	return loaders.GetUser(ctx, obj.UserID)
   192  }
   193  ```
   194  
   195  The end result? Just 2 queries!
   196  ```sql
   197  SELECT id, todo, user_id FROM todo
   198  SELECT id, name from user WHERE id IN (?,?,?,?,?)
   199  ```
   200  
   201  You can see an end-to-end example [here](https://github.com/zenyui/gqlgen-dataloader).