Передача аргументов с использованием Facebook DataLoader

Я использую DataLoader для пакетной обработки запросов/запросов. В моей функции загрузчика мне нужно знать запрошенные поля, чтобы избежать наличия SELECT * FROM query, а скорее SELECT field1, field2, ... FROM query...

Как лучше всего использовать DataLoader для передачи необходимого resolveInfo? (Я использую resolveInfo.fieldNodes, чтобы получить запрошенные поля)

На данный момент я делаю что-то вроде этого:

await someDataLoader.load({ ids, args, context, info });

а затем в самом загрузчикеFn:

const loadFn = async options => {
const ids = [];
let args;
let context;
let info;
options.forEach(a => {
    ids.push(a.ids);
    if (!args && !context && !info) {
        args = a.args;
        context = a.context;
        info = a.info;
    }
});

return Promise.resolve(await new DataProvider().get({ ...args, ids}, context, info));};

но, как вы видите, он хакерский и не очень хорошо себя чувствует...

У кого-нибудь есть идеи, как мне этого добиться?

Ответ 1

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

Давайте разберемся, почему Dataloader не создан для этого варианта использования и как мы все еще можем заставить его работать (примерно как в вашем примере).

Dataloader не предназначен для извлечения подмножества полей

Dataloader предназначен для простого поиска по значению ключа. Это означает, что при наличии ключа, такого как идентификатор, будет загружено значение, стоящее за ним. Для этого предполагается, что объект за идентификатором всегда будет одинаковым, пока он не станет недействительным. Это единственное допущение, которое обеспечивает мощность загрузчика данных. Без этого три ключевых функции Dataloader больше не будут работать:

  1. Пакетные запросы (несколько запросов выполняются вместе в одном запросе)
  2. Дедупликация (запросы к одному и тому же ключу дважды приводят к одному запросу)
  3. Кэширование (последовательные запросы одного и того же ключа не приводят к нескольким запросам)

Это приводит нас к следующим двум важным правилам, если мы хотим максимально использовать возможности Dataloader:

Два разных объекта не могут использовать один и тот же ключ, иначе мы можем вернуть неправильный объект. Это звучит тривиально, но это не в вашем примере. Допустим, мы хотим загрузить пользователя с идентификатором 1 и полями id и name. Чуть позже (или одновременно) мы хотим загрузить пользователя с идентификатором 1 и полями id и email. Технически это две разные сущности, и они должны иметь разные ключи.

Один и тот же объект должен иметь один и тот же ключ все время. Опять звучит тривиально, но на самом деле это не так. Пользователь с идентификатором 1 и полями id и name должен совпадать с пользователем с идентификатором 1 и полями name и id (обратите внимание на порядок).

Короче говоря, ключ должен иметь всю информацию, необходимую для уникальной идентификации объекта, но не более того.

Итак, как нам передать поля в Dataloader

await someDataLoader.load({ ids, args, context, info });

В своем вопросе вы предоставили вашему Dataloader еще несколько вещей в качестве ключа. Сначала я не стал бы вставлять аргументы и контекст в ключ. Меняется ли ваша сущность при изменении контекста (например, вы сейчас запрашиваете другую базу данных)? Возможно, да, но вы хотите учесть это в своей реализации загрузчика данных? Вместо этого я бы предложил создавать новые загрузчики данных для каждого запроса, как описано в документации.

Должна ли вся информация запроса быть в ключе? Нет, но нам нужны поля, которые запрашиваются. Кроме того, предоставленная вами реализация неверна и может прерваться при вызове загрузчика с двумя разными сведениями о разрешении. Вы устанавливаете информацию о разрешении только при первом вызове, но на самом деле она может отличаться для каждого объекта (подумайте о первом примере пользователя выше). В конечном итоге мы можем прийти к следующей реализации загрузчика данных:

// This function creates unique cache keys for different selected
// fields
function cacheKeyFn({ id, fields }) {
  const sortedFields = [...(new Set(fields))].sort().join(';');
  return '${id}[${sortedFields}]';
}

function createLoaders(db) {
  const userLoader = new Dataloader(async keys => {
    // Create a set with all requested fields
    const fields = keys.reduce((acc, key) => {
      key.fields.forEach(field => acc.add(field));
      return acc;
    }, new Set());
    // Get all our ids for the DB query
    const ids = keys.map(key => key.id);
    // Please be aware of possible SQL injection, don't copy + paste
    const result = await db.query('
      SELECT
        ${fields.entries().join()}
      FROM
        user
      WHERE
        id IN (${ids.join()})
    ');
  }, { cacheKeyFn });

  return { userLoader };
}

// now in a resolver
resolve(parent, args, ctx, info) {
  // https://www.npmjs.com/package/graphql-fields
  return ctx.userLoader.load({ id: args.id, fields: Object.keys(graphqlFields(info)) });
}

Это надежная реализация, но у нее есть несколько недостатков. Во-первых, мы перегружаем множество полей, если у нас разные требования к полю в одном и том же пакетном запросе. Во-вторых, если мы извлекли объект с ключом 1[id,name] из функции ключа кэша, мы могли бы также ответить (по крайней мере в JavaScript) на ключи 1[id] и 1[name] с этим объектом. Здесь мы могли бы создать собственную реализацию карты, которую мы могли бы предоставить Dataloader. Было бы достаточно умно знать эти вещи о нашем кеше.

Заключение

Мы видим, что это действительно сложный вопрос. Я знаю, что часто в качестве преимущества GraphQL указано, что вам не нужно извлекать все поля из базы данных для каждого запроса, но правда в том, что на практике это редко стоит хлопот. Не оптимизируйте то, что не медленно. И даже медленно ли это, узкое место?

Я предлагаю: написать тривиальные загрузчики данных, которые просто выбирают все (необходимые) поля. Если у вас есть один клиент, очень вероятно, что для большинства объектов клиент все равно извлекает все поля, иначе они не были бы частью вашего API, верно? Затем используйте что-то вроде интроспекции запроса, чтобы измерить медленные запросы, а затем выясните, какое именно поле является медленным. Затем вы оптимизируете только медленную вещь (см., Например, мой ответ здесь, который оптимизирует один вариант использования). И если вы большая платформа ecomerce, пожалуйста, не используйте Dataloader для этого. Создайте что-нибудь умнее и не используйте JavaScript.