我正在使用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));};
但是正如你所看到的那样,它很黑,而且感觉不太好......
有谁知道如何实现这一目标?
答案 0 :(得分:1)
我不确定这个问题是否有一个很好的答案,仅仅是因为没有为该用例创建Dataloader,而是我与Dataloader进行了广泛的合作,编写了类似的实现并在其他编程语言上探讨了类似的概念。
让我们理解为什么不为该用例创建Dataloader,以及我们如何使它能够正常工作(大致与您的示例类似)。
数据加载器用于简单的键值查找。这意味着给定 key 这样的ID,它将在其后加载一个值。为此,它假定ID后面的对象在失效之前始终是相同的。这是启用数据加载器功能的单一假设。没有它,Dataloader的三个关键功能将不再起作用:
如果要最大化Dataloader的功能,这将导致以下两个重要规则:
两个不同的实体不能共享同一密钥,否则我们可能会返回错误的实体。这听起来微不足道,但在您的示例中却并非如此。假设我们要加载ID为1
且字段为id
和name
的用户。稍后(或同时),我们要加载ID为1
且字段为id
和email
的用户。从技术上讲,这是两个不同的实体,它们需要具有不同的密钥。
同一实体应该始终具有相同的密钥。听起来似乎微不足道,但实际上不在示例中。标识为1
且字段为id
和name
的用户应与标识为1
且字段为name
和id
的用户相同(请注意订单)。
简而言之,密钥需要具有唯一标识一个实体所需的全部信息,但不能超过。
await 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的自定义地图实现。足够了解我们的缓存这些知识就足够了。
我们看到这确实是一件复杂的事情。我知道它通常被列为GraphQL的一个优点,您不必为每个查询都从数据库中获取所有字段,但事实是,在实践中,这很少值得您解决。 不要优化不慢的内容。甚至很慢,这是瓶颈吗?
我的建议是:编写简单的Dataloader,以简单地获取所有(所需)字段。如果您有一个客户端,则对于大多数实体而言,客户端很可能无论如何都会获取所有字段,否则它们将不属于您的API,对吗?然后使用类似查询解释的方法来衡量慢速查询,然后找出哪个字段恰好慢速。然后,您仅优化最慢的事情(例如,参见我的答案here,它优化了单个用例)。如果您是一个大型的ecomerce平台,请不要为此使用Dataloader。构建更智能的东西,不要使用JavaScript。