使用 Facebook 的 DataLoader 传递参数
Passing down arguments using Facebook's DataLoader
我正在使用 DataLoader 将 requests/queries 批处理在一起。
在我的加载程序函数中,我需要知道请求的字段以避免 SELECT * FROM query
而是 SELECT field1, field2, ... FROM query
...
使用 DataLoader 传递它所需的 resolveInfo
的最佳方法是什么? (我使用 resolveInfo.fieldNodes
来获取请求的字段)
目前,我正在做这样的事情:
await someDataLoader.load({ ids, args, context, info });
然后在实际的loaderFn:
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));};
但是如您所见,它很老套而且感觉不太好...
有谁知道我如何实现这一点?
我不确定这个问题是否有一个很好的答案,因为 Dataloader 不是为这个用例而设计的,但我已经广泛使用 Dataloader,编写了类似的实现并探索了其他编程语言的类似概念。
让我们了解为什么 Dataloader 不是为这个用例制作的,以及我们如何仍然可以让它工作(大致就像你的例子一样)。
Dataloader 不是为获取字段子集而设计的
Dataloader 是为简单的键值查找而设计的。这意味着给定一个像 ID 一样的 key 它会在它后面加载一个值。为此,它假定 ID 后面的对象在它失效之前将始终相同。这是启用数据加载器功能的唯一假设。没有它,Dataloader 的三个 关键功能将不再有效:
- 批处理请求(多个请求在一个查询中一起完成)
- 重复数据删除(对同一个键的请求两次导致一次查询)
- 缓存(同一键的连续请求不会导致多次查询)
如果我们想最大限度地发挥 Dataloader 的功能,这将引导我们遵循以下两个重要规则:
两个不同的实体不能共享相同的密钥,否则我们可能会return错误的实体。这听起来微不足道,但它不在您的示例中。假设我们要加载 ID 为 1
且字段为 id
和 name
的用户。稍后(或同时)我们要加载 ID 为 1
且字段为 id
和 email
的用户。从技术上讲,它们是两个不同的实体,它们需要不同的密钥。
同一个实体应该始终具有相同的密钥。再次听起来微不足道,但实际上不在示例中。具有 ID 1
和字段 id
和 name
的用户应该与具有 ID 1
和字段 name
和 id
的用户相同(注意订单)。
简而言之一个键需要包含唯一标识一个实体所需的所有信息,但不能超过。
那么我们如何将字段传递给 Dataloader
await someDataLoader.load({ ids, args, context, info });
在您的问题中,您向 Dataloader 提供了更多内容作为密钥。首先,我不会将参数和上下文放入密钥中。当上下文发生变化时,您的实体是否会发生变化(例如,您现在正在查询不同的数据库)?可能是的,但是你想在你的数据加载器实现中考虑到这一点吗?相反,我建议按照 docs.
中所述为每个请求创建新的数据加载器
整个请求信息应该在密钥中吗?不,但我们需要请求的字段。除此之外,您提供的实现是错误的,并且会在使用两个不同的解析信息调用加载程序时中断。您只设置第一次调用的解析信息,但实际上它可能在每个对象上都不同(想想上面的第一个用户示例)。最终我们可以实现数据加载器的以下实现:
// 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 的一个好处,即您不必为每个查询从数据库中获取所有字段,但事实是,在实践中,这很少值得麻烦。 不优化不慢。甚至慢,是瓶颈吗?
我的建议是:编写简单的 Dataloader 来简单地获取所有(需要的)字段。如果您有一个客户端,很可能对于大多数实体而言,该客户端无论如何都会获取所有字段,否则它们就不会成为您的一部分 API,对吗?然后使用查询自省之类的东西来衡量慢速查询,然后找出到底是哪个字段慢。然后你只优化慢的东西(例如参见我的答案 优化单个用例)。如果您是大型电子商务平台,请不要为此使用 Dataloader。构建更智能的东西,不要使用 JavaScript.
我正在使用 DataLoader 将 requests/queries 批处理在一起。
在我的加载程序函数中,我需要知道请求的字段以避免 SELECT * FROM query
而是 SELECT field1, field2, ... FROM query
...
使用 DataLoader 传递它所需的 resolveInfo
的最佳方法是什么? (我使用 resolveInfo.fieldNodes
来获取请求的字段)
目前,我正在做这样的事情:
await someDataLoader.load({ ids, args, context, info });
然后在实际的loaderFn:
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));};
但是如您所见,它很老套而且感觉不太好...
有谁知道我如何实现这一点?
我不确定这个问题是否有一个很好的答案,因为 Dataloader 不是为这个用例而设计的,但我已经广泛使用 Dataloader,编写了类似的实现并探索了其他编程语言的类似概念。
让我们了解为什么 Dataloader 不是为这个用例制作的,以及我们如何仍然可以让它工作(大致就像你的例子一样)。
Dataloader 不是为获取字段子集而设计的
Dataloader 是为简单的键值查找而设计的。这意味着给定一个像 ID 一样的 key 它会在它后面加载一个值。为此,它假定 ID 后面的对象在它失效之前将始终相同。这是启用数据加载器功能的唯一假设。没有它,Dataloader 的三个 关键功能将不再有效:
- 批处理请求(多个请求在一个查询中一起完成)
- 重复数据删除(对同一个键的请求两次导致一次查询)
- 缓存(同一键的连续请求不会导致多次查询)
如果我们想最大限度地发挥 Dataloader 的功能,这将引导我们遵循以下两个重要规则:
两个不同的实体不能共享相同的密钥,否则我们可能会return错误的实体。这听起来微不足道,但它不在您的示例中。假设我们要加载 ID 为 1
且字段为 id
和 name
的用户。稍后(或同时)我们要加载 ID 为 1
且字段为 id
和 email
的用户。从技术上讲,它们是两个不同的实体,它们需要不同的密钥。
同一个实体应该始终具有相同的密钥。再次听起来微不足道,但实际上不在示例中。具有 ID 1
和字段 id
和 name
的用户应该与具有 ID 1
和字段 name
和 id
的用户相同(注意订单)。
简而言之一个键需要包含唯一标识一个实体所需的所有信息,但不能超过。
那么我们如何将字段传递给 Dataloader
await someDataLoader.load({ ids, args, context, info });
在您的问题中,您向 Dataloader 提供了更多内容作为密钥。首先,我不会将参数和上下文放入密钥中。当上下文发生变化时,您的实体是否会发生变化(例如,您现在正在查询不同的数据库)?可能是的,但是你想在你的数据加载器实现中考虑到这一点吗?相反,我建议按照 docs.
中所述为每个请求创建新的数据加载器整个请求信息应该在密钥中吗?不,但我们需要请求的字段。除此之外,您提供的实现是错误的,并且会在使用两个不同的解析信息调用加载程序时中断。您只设置第一次调用的解析信息,但实际上它可能在每个对象上都不同(想想上面的第一个用户示例)。最终我们可以实现数据加载器的以下实现:
// 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 的一个好处,即您不必为每个查询从数据库中获取所有字段,但事实是,在实践中,这很少值得麻烦。 不优化不慢。甚至慢,是瓶颈吗?
我的建议是:编写简单的 Dataloader 来简单地获取所有(需要的)字段。如果您有一个客户端,很可能对于大多数实体而言,该客户端无论如何都会获取所有字段,否则它们就不会成为您的一部分 API,对吗?然后使用查询自省之类的东西来衡量慢速查询,然后找出到底是哪个字段慢。然后你只优化慢的东西(例如参见我的答案