Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example to use with graphql-go #19

Closed
vietthang opened this issue Apr 3, 2017 · 8 comments
Closed

Example to use with graphql-go #19

vietthang opened this issue Apr 3, 2017 · 8 comments

Comments

@vietthang
Copy link

I'm building a project with graphql-go and need a dataloader implementation. After a bit research, I found your library and seems like you build this with graphql-go support in mind. But I don't understand how to use 2 libraries together. Can you help me with some sample code or ideas how they work together. Do I need to modify graphql-go source code to use this library?

Thank you.

@tonyghita
Copy link
Member

Do I need to modify graphql-go source code to use this library?

@vietthang definitely not. This library is meant to be orthogonal to any other library, but I can give you an example of how you could use them together.

One way to do it is to attach the data loaders to the request context, and extract them from the context when you need them in your resolver (since each resolver has context.Context as an optional first parameter).

I'm currently using the library this way, but I suspect it's an abuse of the request context and that there's a much better way to pass the data loaders around.

Example

GraphQL schema

query {
  # Get a thing by it's ID.
  thing(id: ID!): Thing
}

# Thing is pretty simple.
type Thing {
  id: ID
  name: String
}

Loader

// ThingsLoader probably holds whatever we will use to load Things.
type ThingsLoader struct {
  client things.Client
}

func (l *ThingsLoader) Attach(ctx context.Context) dataloader.BatchFunc {
  return func(ids []string) []*dataloader.Result {
    resp, err := l.client.Things(ctx, ids)
    if err != nil {
      // return a `[]*dataloader.Result` with `len(ids)` elements that have errors.
    }
    // do any work you need to do to get the response in the same order as the input `ids`
    results := []*dataloader.Result{}
    for _, thing := range resp.Things {
      results = append(results, thing)
    }
    return results, nil
  }
}

Resolvers

// ThingResolver holds information about the thing we are resolving.
type ThingResolver struct {
  id     string
  loader *dataloader.Loader // we'll get this from the request context
}

// ThingArgs holds the arguments you pass into the `thing` query.
type ThingArgs struct {
  ID graphql.ID
}

// Thing resolves the query `thing`.
func (r *QueryResolver) Thing(ctx context.Context, args *ThingArgs) (*ThingResolver, error) {
  // Here we are extracting the loader that we've placed on the context at the 
  // beginning of the request, and asserting the type of the value is `*dataloader.Loader`.
  loader, found := ctx.Value("thing loader").(*dataloader.Loader)
  if !found {
    return nil, errors.New("unable to find the thing loader")
  }

  return &ThingResolver{id: args.ID, loader: loader}, nil
}

// ID is the Thing's identifier.
// We already have it, so let's not load any data.
func (r *ThingResolver) ID() *graphql.ID {
  id := graphql.ID(r.ID)
  return &id
}

// Name is what we call this Thing.
// We don't know it yet, so we have to load it.
func (r *ThingResolver) Name() (*string, error) {
  thunk := r.loader.Load(r.ID)
  data, err := thunk()
  if err != nil {
    return nil, fmt.Errorf("thing.name: %v", err)
  }

  name, ok := data.(*string)
  if !ok {
    return nil, fmt.Errorf("thing.name: loaded the wrong type of data: %#v", data)
  }

  return name, nil
}

Obviously if you are loading data in multiple resolvers, you'll want to pull that code into it's own function, but that should give you an idea.

Tying it all together

func handler(w http.ResponseWriter, req *http.Request) {
  ctx := req.Context()
  thingLoader := &ThingLoader{client: thing.NewClient()}
  batchFunc := thingLoader.Attach(ctx)

  loader := dataloader.NewBatchedLoader(batchFunc)
  ctx = context.WithValue(ctx, "thing loader", loader)
  
  // Execute the query against your schema and resolver
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":12345", nil))
}

Another way you could go is to initialize the data loader where you need it (and possibly cache the instance if it's expensive to initialize the loader).

Maybe a better way would be to have some data structure with all your loaders, and then pass around a reference to that data structure in your resolvers.

I'm 95% sure the example I gave you is not the best approach to passing loaders around.

I'll get a chance to revisit other implementations sooner than later, but I encourage you to explore a few patterns, see what works for you, and share your results 😄

@vietthang
Copy link
Author

Thank you very much for your detailed answer. I will try your approach and definitely will share if I come up with any idea. Thanks.

@gburt
Copy link

gburt commented Mar 24, 2018

For those using graphql-go, this approach doesn't work (as of this date) because graphql-go resolves field serially. Follow graphql-go/graphql#106

@vietthang
Copy link
Author

@gburt Do you have any idea how the parallel field resolving thing in graphql-go go? I had to drop using graphql-go and use node.js because of this issue a quite time ago.

@dvic
Copy link

dvic commented Mar 24, 2018

@gburt @vietthang You can try https://github.com/qdentity/graphql-go, but please be aware I'm not maintaining this fork. I took the changes from graphql-go/graphql#213 and added the concurrent resolving of list values graphql-go/graphql#132. There were some data races but I managed to fix these as well, all tests are passing with the -race flag included.

Currently we went for https://github.com/graph-gophers/graphql-go as it supports dataloader out-of-the-box (it resolves fields already concurrently).

@smithaitufe
Copy link

smithaitufe commented Apr 24, 2018

What is this thing about this NewClient? What purpose does it serve? I am trying to grasp it.

type ThingsLoader struct {
  client things.Client
}

@oskanberg
Copy link

@smithaitufe I think that is supposed to represent a client to load information about things. In this case it is a client with method Things(ctx, ids) []Things - it could be a http client or similar.

@chris-ramon
Copy link
Contributor

For those using graphql-go, this approach doesn't work (as of this date) because graphql-go resolves field serially. Follow graphql-go/graphql#106

For those looking for a full working example, you might want to take a look to: https://github.com/graphql-go/graphql-dataloader-sample

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants