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
};