Получение данных GraphQL на уровне запроса приводит к избыточному / бесполезному запросу

Мы внедряем сервис GraphQL, который стоит перед несколькими внутренними микросервисами.

Например, у нас есть Product, и у каждого продукта есть список заказов в истории. Наш внутренний сервер предоставляет два REST API, один для данных о продукте, а другой возвращает список заказов в истории продукта.

В нашем клиентском приложении есть две страницы: одна - это страница с подробностями о продукте, другая - это история заказа продукта.

Таким образом, на странице сведений о продукте мы можем получить только подробные данные о продукте, в то время как на странице со списком заказов нам нужны только данные списка.

Схема 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 списка заказов. Но нам ТОЛЬКО нужны данные списка заказов, а не данные о продукте вообще. В этом случае мы считаем, что запрос сведений о продукте бесполезен, как мы можем устранить этот запрос?

Было бы лучше, если бы мы могли отправить только один запрос для получения данных списка заказов.


person Jess    schedule 18.09.2019    source источник


Ответы (1)


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

Б) Пропустить выборку, если подробности не запрашиваются

Вы можете использовать четвертый аргумент преобразователя для проверки запрошенных подполей. В идеале для этого нужно использовать библиотеку. Я помню, как мы делали это, когда наш интерфейс запрашивал только поле 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. Вместо того, чтобы сразу же получать детали в преобразователе запросов, вы снова передаете идентификатор и позволяете каждому из полей деталей получать детали сами. Это кажется нелогичным, но 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
};
person Herku    schedule 18.09.2019
comment
Спасибо. Метод C похож на способ, который PayPal использовал в своей статье лучшие-практики графических преобразователей. Но явное написание преобразователя для каждого поля как-то дублирует, особенно когда дело касается сложного Type со слишком большим количеством полей. - person Jess; 19.09.2019
comment
Да, возможно, но я думаю, что этот преобразователь по умолчанию очень уникален для JavaScript. Метод C - это также то, как я понимаю код GraphQL Facebook, написанный на Hack. Я думаю, что в настоящее время они генерируют много этого шаблона, но я думаю, что чем больше проект, тем менее проблематичным является явный код. - person Herku; 19.09.2019
comment
Значит, Facebook также использует метод C? Кажется, я тоже должен идти по этому пути. Кстати, Facebook открывает исходный код некоторых из своего кода GraphQL? Я не нашел. - person Jess; 19.09.2019
comment
Ну, я не знаю, что они делают, но Дэн Шафер показывает нечто подобное в разговоре (особенно 22:15). Есть также больше разговоров с конференции от людей FB. Я бы не стал над этим задумываться, просто выбрал что-то и изменил, как только вам больше понравится другой вариант. В FB рассказывают о том, как они снова и снова меняют свой образ действий. - person Herku; 19.09.2019