使用 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 的三个 关键功能将不再有效:

  1. 批处理请求(多个请求在一个查询中一起完成)
  2. 重复数据删除(对同一个键的请求两次导致一次查询)
  3. 缓存(同一键的连续请求不会导致多次查询)

如果我们想最大限度地发挥 Dataloader 的功能,这将引导我们遵循以下两个重要规则:

两个不同的实体不能共享相同的密钥,否则我们可能会return错误的实体。这听起来微不足道,但它不在您的示例中。假设我们要加载 ID 为 1 且字段为 idname 的用户。稍后(或同时)我们要加载 ID 为 1 且字段为 idemail 的用户。从技术上讲,它们是两个不同的实体,它们需要不同的密钥。

同一个实体应该始终具有相同的密钥。再次听起来微不足道,但实际上不在示例中。具有 ID 1 和字段 idname 的用户应该与具有 ID 1 和字段 nameid 的用户相同(注意订单)。

简而言之一个键需要包含唯一标识一个实体所需的所有信息,但不能超过

那么我们如何将字段传递给 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.