我正在使用DataLoader将请求/查询一起批处理。
在我的加载器函数中,我需要知道请求的字段,以避免产生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并非用于获取字段的子集
数据加载器用于简单的键值查找。这意味着给定像ID这样的键,它将在其后加载一个值。为此,它假定ID后面的对象在失效之前始终是相同的。这是启用数据加载器功能的单一假设。没有它,Dataloader 的三个关键功能将不再起作用:
如果要最大化Dataloader的功能,这将导致我们遵循以下两个重要规则:
两个不同的实体不能共享相同的密钥,否则我们可能会返回错误的实体。这听起来微不足道,但在您的示例中却并非如此。假设我们要加载ID为1
以及字段id
和name
的用户。稍后(或同时),我们希望向用户加载ID 1
以及字段id
和email
。从技术上讲,这是两个不同的实体,它们需要具有不同的密钥。
相同实体在所有时间都应具有相同的密钥。听起来似乎微不足道,但实际上不在示例中。 ID为1
且字段id
和name
的用户应与ID为1
且字段name
和id
的用户相同(请注意顺序)。
简而言之,密钥需要具有唯一标识一个实体所需的所有信息,但不能超过。
那么我们如何将字段传递给Dataloaderawait someDataLoader.load({ ids, args, context, info });
在您的问题中,您已为Dataloader提供了一些其他的关键信息。首先,我不会将args和context放入键中。当上下文改变时(例如,您现在正在查询其他数据库),您的实体会改变吗?可能是的,但是您想在数据加载器实现中考虑到这一点吗?相反,我建议为每个请求创建新的数据加载器,如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的自定义 map 实现。知道有关缓存的这些知识将足够聪明。
结论
我们看到这确实是一件复杂的事情。我知道它通常被列为GraphQL的一个优点,您不必为每个查询都从数据库中获取所有字段,但事实是,在实践中,这很少值得您解决。 不要优化不慢的内容。甚至很慢,这是瓶颈吗?
我的建议是:编写简单的Dataloader,以简单地获取所有(所需)字段。如果您有一个客户端,则对于大多数实体而言,客户端很可能无论如何都会获取所有字段,否则它们将不属于您的API,对吗?然后使用类似查询解释的方法来衡量慢速查询,然后找出哪个字段恰好是慢速。然后,您仅优化慢的事情(例如,参见我的回答here,它优化了单个用例)。如果您是一个大型的ecomerce平台,请不要为此使用Dataloader。构建更智能的东西,不要使用JavaScript。