GraphQL 在查询级别获取数据导致 redundant/useless 请求
GraphQL fetch data at Query level results in redundant/useless request
我们正在实施 GraphQL 服务,它位于几个后端微服务的前面。
比如我们有一个Product
,每个产品都有一个历史订单列表。我们的后端服务器提供两个 REST APIs,一个用于产品详细数据,另一个returns产品的历史订单列表。
我们的客户端应用程序有两个页面:一个是产品详情页面,另一个是产品的历史订单列表。
所以,在商品详情页,我们只能获取商品的详情数据,而在订单列表页,我们只需要列表数据。
GraphQL 架构如下:
type ProductOrder {
createAt: Date!
userName: String!
count: Int
}
type Product {
productId: ID
name: String
orders: [ProductOrder!]!
}
Query {
product(productId: ID): Product
}
解析器是这样的
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);
}
}
};
但我们发现使用上述代码可能存在过度请求。
当我们从订单列表页面请求订单列表数据时,我们必须先请求产品详细信息API,然后我们才能请求订单列表API。但是我们只需要订单列表数据,根本不需要产品数据。在这种情况下,我们认为产品详情请求没有用,我们如何消除这个请求?
如果我们可以只发送一个请求来检索订单列表数据,那就更好了。
A) 以不同方式构建您的架构:
版本 1:不要将 ProductOrder 设为 Product
的字段
type Query {
product(productId: ID): Product
productOrders(productId: ID): [ProductOrder!]
}
type Product {
productId: ID
name: String
}
版本 2:将详细信息作为产品中的子字段
type Product {
productId: ID
details: ProductDetails!
orders: [ProductOrder!]!
}
type ProductDetails {
name: String
}
使用解析器:
const resolvers = {
Query: {
product: (_, { productId }) => productId,
},
Product: {
id: productId => productId,
details: productId => someService.getProductDetail(productId),
orders: productId => someService.getProductOrders(productId),
},
};
B) 如果没有请求详细信息,则跳过提取
您可以使用解析器的第四个参数来检查查询的子字段。理想情况下,您为此使用一个库。我记得我们在前端只请求对象的 id
字段时这样做。如果是这样,我们可以简单地用 { 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) 延迟提取直到子字段被查询
如果您已经在使用 Dataloader,这相对容易做到。不是立即在查询解析器中获取详细信息,而是再次传递 id 并让每个详细信息字段自己获取详细信息。这似乎违反直觉,但 Dataloader 将确保您的服务仅被查询一次:
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),
},
};
如果您没有数据加载器,您可以构建一个简单的代理:
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
};
我们正在实施 GraphQL 服务,它位于几个后端微服务的前面。
比如我们有一个Product
,每个产品都有一个历史订单列表。我们的后端服务器提供两个 REST APIs,一个用于产品详细数据,另一个returns产品的历史订单列表。
我们的客户端应用程序有两个页面:一个是产品详情页面,另一个是产品的历史订单列表。
所以,在商品详情页,我们只能获取商品的详情数据,而在订单列表页,我们只需要列表数据。
GraphQL 架构如下:
type ProductOrder {
createAt: Date!
userName: String!
count: Int
}
type Product {
productId: ID
name: String
orders: [ProductOrder!]!
}
Query {
product(productId: ID): Product
}
解析器是这样的
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);
}
}
};
但我们发现使用上述代码可能存在过度请求。
当我们从订单列表页面请求订单列表数据时,我们必须先请求产品详细信息API,然后我们才能请求订单列表API。但是我们只需要订单列表数据,根本不需要产品数据。在这种情况下,我们认为产品详情请求没有用,我们如何消除这个请求?
如果我们可以只发送一个请求来检索订单列表数据,那就更好了。
A) 以不同方式构建您的架构:
版本 1:不要将 ProductOrder 设为 Product
的字段type Query {
product(productId: ID): Product
productOrders(productId: ID): [ProductOrder!]
}
type Product {
productId: ID
name: String
}
版本 2:将详细信息作为产品中的子字段
type Product {
productId: ID
details: ProductDetails!
orders: [ProductOrder!]!
}
type ProductDetails {
name: String
}
使用解析器:
const resolvers = {
Query: {
product: (_, { productId }) => productId,
},
Product: {
id: productId => productId,
details: productId => someService.getProductDetail(productId),
orders: productId => someService.getProductOrders(productId),
},
};
B) 如果没有请求详细信息,则跳过提取
您可以使用解析器的第四个参数来检查查询的子字段。理想情况下,您为此使用一个库。我记得我们在前端只请求对象的 id
字段时这样做。如果是这样,我们可以简单地用 { 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) 延迟提取直到子字段被查询
如果您已经在使用 Dataloader,这相对容易做到。不是立即在查询解析器中获取详细信息,而是再次传递 id 并让每个详细信息字段自己获取详细信息。这似乎违反直觉,但 Dataloader 将确保您的服务仅被查询一次:
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),
},
};
如果您没有数据加载器,您可以构建一个简单的代理:
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
};