github.com/99designs/gqlgen@v0.17.45/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 [vikstrous/dataloadgen](https://github.com/vikstrous/dataloadgen) to implement a dataloader for bulk-fetching users.
    67  
    68  ```bash
    69  go get github.com/vikstrous/dataloadgen
    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 vikstrous/dataloadgen with your other imports
    78  import (
    79  	"context"
    80  	"database/sql"
    81  	"net/http"
    82  	"strings"
    83  	"time"
    84  
    85  	"github.com/vikstrous/dataloadgen"
    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) ([]*model.User, []error) {
   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 nil, []error{err}
   105  	}
   106  	defer stmt.Close()
   107  
   108  	rows, err := stmt.QueryContext(ctx, userIDs)
   109  	if err != nil {
   110  		return nil, []error{err}
   111  	}
   112  	defer rows.Close()
   113  
   114  	users := make([]*model.User, 0, len(userIDs))
   115  	errs := make([]error, 0, len(userIDs))
   116  	for rows.Next() {
   117  		var user model.User
   118  		if err := rows.Scan(&user.ID, &user.Name); err != nil {
   119  			errs = append(errs, err)
   120  			continue
   121  		}
   122  		users = append(users, &user)
   123  	}
   124  	return users, errs
   125  }
   126  
   127  // Loaders wrap your data loaders to inject via middleware
   128  type Loaders struct {
   129  	UserLoader *dataloadgen.Loader[string, *model.User]
   130  }
   131  
   132  // NewLoaders instantiates data loaders for the middleware
   133  func NewLoaders(conn *sql.DB) *Loaders {
   134  	// define the data loader
   135  	ur := &userReader{db: conn}
   136  	return &Loaders{
   137  		UserLoader: dataloadgen.NewLoader(ur.getUsers, dataloadgen.WithWait(time.Millisecond)),
   138  	}
   139  }
   140  
   141  // Middleware injects data loaders into the context
   142  func Middleware(conn *sql.DB, next http.Handler) http.Handler {
   143  	// return a middleware that injects the loader to the request context
   144  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   145  		loader := NewLoaders(conn)
   146  		r = r.WithContext(context.WithValue(r.Context(), loadersKey, loader))
   147  		next.ServeHTTP(w, r)
   148  	})
   149  }
   150  
   151  // For returns the dataloader for a given context
   152  func For(ctx context.Context) *Loaders {
   153  	return ctx.Value(loadersKey).(*Loaders)
   154  }
   155  
   156  // GetUser returns single user by id efficiently
   157  func GetUser(ctx context.Context, userID string) (*model.User, error) {
   158  	loaders := For(ctx)
   159  	return loaders.UserLoader.Load(ctx, userID)
   160  }
   161  
   162  // GetUsers returns many users by ids efficiently
   163  func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, error) {
   164  	loaders := For(ctx)
   165  	return loaders.UserLoader.LoadAll(ctx, userIDs)
   166  }
   167  
   168  ```
   169  
   170  Add the dataloader middleware to your server...
   171  ```go
   172  // create the query handler
   173  var srv http.Handler = handler.NewDefaultServer(generated.NewExecutableSchema(...))
   174  // wrap the query handler with middleware to inject dataloader in requests.
   175  // pass in your dataloader dependencies, in this case the db connection.
   176  srv = loaders.Middleware(db, srv)
   177  // register the wrapped handler
   178  http.Handle("/query", srv)
   179  ```
   180  
   181  Now lets update our resolver to call the dataloader:
   182  ```go
   183  func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
   184  	return loaders.GetUser(ctx, obj.UserID)
   185  }
   186  ```
   187  
   188  The end result? Just 2 queries!
   189  ```sql
   190  SELECT id, todo, user_id FROM todo
   191  SELECT id, name from user WHERE id IN (?,?,?,?,?)
   192  ```
   193  
   194  You can see an end-to-end example [here](https://github.com/vikstrous/dataloadgen-example).