Ответ 1
Я не уверен, что есть хороший ответ на этот вопрос просто потому, что Dataloader не предназначен для этого сценария использования, но я много работал с Dataloader, написал аналогичные реализации и исследовал аналогичные концепции на других языках программирования.
Давайте разберемся, почему Dataloader не создан для этого варианта использования и как мы все еще можем заставить его работать (примерно как в вашем примере).
Dataloader не предназначен для извлечения подмножества полей
Dataloader предназначен для простого поиска по значению ключа. Это означает, что при наличии ключа, такого как идентификатор, будет загружено значение, стоящее за ним. Для этого предполагается, что объект за идентификатором всегда будет одинаковым, пока он не станет недействительным. Это единственное допущение, которое обеспечивает мощность загрузчика данных. Без этого три ключевых функции Dataloader больше не будут работать:
- Пакетные запросы (несколько запросов выполняются вместе в одном запросе)
- Дедупликация (запросы к одному и тому же ключу дважды приводят к одному запросу)
- Кэширование (последовательные запросы одного и того же ключа не приводят к нескольким запросам)
Это приводит нас к следующим двум важным правилам, если мы хотим максимально использовать возможности 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.