问题
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.
回答1:
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
};
来源:https://stackoverflow.com/questions/57988750/graphql-fetch-data-at-query-level-results-in-redundant-useless-request