Skip to content

Commit

Permalink
update example for dataloadgen
Browse files Browse the repository at this point in the history
  • Loading branch information
vikstrous committed Aug 26, 2023
1 parent 065aea3 commit 8a97200
Showing 1 changed file with 21 additions and 48 deletions.
69 changes: 21 additions & 48 deletions docs/content/reference/dataloaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,28 @@ Whats even worse? most of those todos are all owned by the same user! We can do

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.

We're going to use [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader) to implement a dataloader for bulk-fetching users.
We're going to use [vikstrous/dataloadgen](https://github.com/vikstrous/dataloadgen) to implement a dataloader for bulk-fetching users.

```bash
go get -u github.com/graph-gophers/dataloader/v7
go get github.com/vikstrous/dataloadgen
```

Next, we implement a data loader and a middleware for injecting the data loader on a request context.

```go
package loaders

// import graph gophers with your other imports
import (
"context"
"database/sql"
"net/http"
"strings"
"time"

"github.com/graph-gophers/dataloader/v7"
"github.com/vikstrous/dataloadgen"
)

type ctxKey string

const (
loadersKey = ctxKey("dataloaders")
)
type loadersKey struct{}

// userReader reads Users from a database
type userReader struct {
Expand All @@ -98,78 +93,58 @@ type userReader struct {

// getUsers implements a batch function that can retrieve many users by ID,
// for use in a dataloader
func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] {
stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIds)-1)+`)`)
func (u *userReader) getUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) {
stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIDs)-1)+`)`)
if err != nil {
return handleError[*model.User](len(userIds), err)
return nil, []error{err}
}
defer stmt.Close()

rows, err := stmt.QueryContext(ctx, userIds)
rows, err := stmt.QueryContext(ctx, userIDs)
if err != nil {
return handleError[*model.User](len(userIds), err)
return nil, []error{err}
}
defer rows.Close()

result := make([]*dataloader.Result[*model.User], 0, len(userIds))
users := make([]*model.User, 0, len(userIDs))
errs := make([]error, 0, len(userIDs))
for rows.Next() {
var user model.User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
result = append(result, &dataloader.Result[*model.User]{Error: err})
errs = append(errs, err)
continue
}
result = append(result, &dataloader.Result[*model.User]{Data: &user})
}
return result
}

// handleError creates array of result with the same error repeated for as many items requested
func handleError[T any](itemsLength int, err error) []*dataloader.Result[T] {
result := make([]*dataloader.Result[T], itemsLength)
for i := 0; i < itemsLength; i++ {
result[i] = &dataloader.Result[T]{Error: err}
users = append(users, &user)
}
return result
return users, errors
}

// Loaders wrap your data loaders to inject via middleware
type Loaders struct {
UserLoader *dataloader.Loader[string, *model.User]
User *dataloadgen.Loader[string, *model.User]
}

// NewLoaders instantiates data loaders for the middleware
func NewLoaders(conn *sql.DB) *Loaders {
// define the data loader
ur := &userReader{db: conn}
return &Loaders{
UserLoader: dataloader.NewBatchedLoader(ur.getUsers, dataloader.WithWait[string, *model.User](time.Millisecond)),
User: dataloadgen.NewLoader(ur.getUsers, dataloadgen.WithWait(time.Millisecond)),
}
}

// Middleware injects data loaders into the context
func Middleware(loaders *Loaders, next http.Handler) http.Handler {
// return a middleware that injects the loader to the request context
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), loadersKey, loaders))
r = r.WithContext(context.WithValue(r.Context(), loadersKey{}, loaders))
next.ServeHTTP(w, r)
})
}

// For returns the dataloader for a given context
func For(ctx context.Context) *Loaders {
return ctx.Value(loadersKey).(*Loaders)
}

// GetUser returns single user by id efficiently
func GetUser(ctx context.Context, userID string) (*model.User, error) {
loaders := For(ctx)
return loaders.UserLoader.Load(ctx, userID)()
}

// GetUsers returns many users by ids efficiently
func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) {
loaders := For(ctx)
return loaders.UserLoader.LoadMany(ctx, userIDs)()
// Get returns the dataloader for a given context
func Get(ctx context.Context) *Loaders {
return ctx.Value(loadersKey{}).(*Loaders)
}

```
Expand All @@ -188,7 +163,7 @@ http.Handle("/query", srv)
Now lets update our resolver to call the dataloader:
```go
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
return loaders.GetUser(ctx, obj.UserID)
return loaders.Get(ctx).User.LoadAll(ctx, obj.UserID)
}
```

Expand All @@ -197,5 +172,3 @@ The end result? Just 2 queries!
SELECT id, todo, user_id FROM todo
SELECT id, name from user WHERE id IN (?,?,?,?,?)
```

You can see an end-to-end example [here](https://github.com/zenyui/gqlgen-dataloader).

0 comments on commit 8a97200

Please sign in to comment.