GraphQL fetch data at Query level results in redundant/useless request
Asked Answered
R

1

1

We are implementing GraphQL service, which stands in front of several backend microservices.

For example, we have a Product and each product has a list of history orders. Our backend server provides two REST APIs, one for product detail data, the other returns the history order list of a product.

Our client app has two pages: one is the product detail page, the other is the history order list of a product.

So, in the product detail page, we can only retrieve the detail data of the product, while in the order list page, we only need the list data.

The GraphQL schema like below:

type ProductOrder {
    createAt: Date!
    userName: String!
    count: Int
}
type Product {
    productId: ID
    name: String
    orders: [ProductOrder!]!
}
Query {
    product(productId: ID): Product
}

and resolvers are like this

const resolvers = {
    Query: {
        product(_, { productId}){
            // fetch detail data from backend API
            return await someService.getProductDetail(productId);
        }
    },
    Product: {
        orders(product){
            // fetch order list from another API
            return await someService.getProductOrders(product.productId);
        }
    }
};

But we find a potential over-request using the above code.

When we request the order list data from the order list page, we have to request the product detail API first, after then we can request the order list API. But we ONLY need the order list data, no product data at all. In this case, we think the product detail request is useless, how can we eliminate this request?

It could be better if we can send only one request to retrieve the order list data.

Remission answered 18/9, 2019 at 8:51 Comment(0)
H
2

A) Structure your schema differently:

Version 1: Don't make ProductOrder a field on Product

type Query {
  product(productId: ID): Product
  productOrders(productId: ID): [ProductOrder!]
}

type Product {
  productId: ID
  name: String
}

Version 2: Make details a subfield in Product

type Product {
    productId: ID
    details: ProductDetails!
    orders: [ProductOrder!]!
}

type ProductDetails {
  name: String
}

With resolvers:

const resolvers = {
  Query: {
    product: (_, { productId }) => productId,
  },
  Product: {
    id: productId => productId,
    details: productId => someService.getProductDetail(productId),
    orders: productId => someService.getProductOrders(productId),
  },
};

B) Skip fetch if no details are requested

You can use the fourth argument to the resolver to inspect the queried subfields. Ideally you use a library for that. I remember us doing that when our frontend would only request the id field of an object. If so we could simply resolve with { id }.

import { fieldList } from 'graphql-fields-list';

const resolvers = {
  Query: {
    product(_, { productId }, ctx, resolveInfo) {
      const fields = fieldList(resolveInfo);
      if (fields.filter(f => f !== 'orders' || f !== 'id').length === 0) {
        return { productId };
      }
      return someService.getProductDetail(productId);
    },
  },
};

C) Delay fetch until subfield is queried

This is relatively easy to do if you are already using Dataloader. Instead of fetching the details right away in the query resolver you again pass down the id and let each of the details fields fetch the details themselves. This seems counterintuitve but Dataloader will make sure your service is only queried once:

const resolvers = {
  Query: {
    product: (_, { productId }) => productId,
  },
  Product: {
    id: productId => productId,
    // same for all other details fields
    name: (productId, args, ctx) => ctx.ProductDetailsByIdLoader.load(productId)
      .then(product => product.name),
    orders: productId => someService.getProductOrders(productId),
  },
};

If you don't have dataloader you can build a simple proxy:

class ProductProxy {
  constructor(id) {
    this.id = id;
    let cached = null;
    this.getDetails = () => {
      if (cached === null) {
        cached = someService.getProductDetails(productId)
      }
      return cached;
    }
  }
  // args not needed but for you to see how graphql-js works
  productId(args, ctx, resolveInfo) {
    return this.id;
  }
  name(args, ctx, resolveInfo) {
    return this.getDetails().then(details => details.name);
  }
  orders(args, ctx, resolveInfo) {
    return someService.getProductOrders(this.id);
  }
}

const resolvers = {
  Query: {
    product: (_, { productId }) => new ProductProxy(productId),
  },
  // No product resolvers need here
};
Hartzel answered 18/9, 2019 at 13:18 Comment(4)
Thx. The method C seems like the way PayPal used in their article graphql-resolvers-best-practices. But writing a resolver for every field explicitly is somehow duplicate, especially when it comes to a complex Type with too many fields.Remission
Yes maybe but I think this default resolver thing is very unique to JavaScript. Method C is also how I understand Facebooks GraphQL code which is written in Hack. I think nowadays they generate a lot of this boilerplate but I think the bigger the project the less problematic is explicit code.Hartzel
So Facebook also uses the method C? It seems I have to follow this way too. BTW, does Facebook open source some of their GraphQL code? I didn't find it.Remission
Well I don't know what they are doing but Dan Schafer shows something similar in a talk (esp. 22:15). There are also more talks from the conference from FB people. I would not overthink it, just chose something and change it once you like a different way more. The FB talks talk about how they have changed their way of doing things over and over again.Hartzel

© 2022 - 2024 — McMap. All rights reserved.