我正在使用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以及字段idname的用户。稍后(或同时),我们希望向用户加载ID 1以及字段idemail。从技术上讲,这是两个不同的实体,它们需要具有不同的密钥。

    相同实体在所有时间都应具有相同的密钥。听起来似乎微不足道,但实际上不在示例中。 ID为1且字段idname的用户应与ID为1且字段nameid的用户相同(请注意顺序)。

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

    那么我们如何将字段传递给Dataloader
    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的自定义 map 实现。知道有关缓存的这些知识将足够聪明。

    结论

    我们看到这确实是一件复杂的事情。我知道它通常被列为GraphQL的一个优点,您不必为每个查询都从数据库中获取所有字段,但事实是,在实践中,这很少值得您解决。 不要优化不慢的内容。甚至很慢,这是瓶颈吗?

    我的建议是:编写简单的Dataloader,以简单地获取所有(所需)字段。如果您有一个客户端,则对于大多数实体而言,客户端很可能无论如何都会获取所有字段,否则它们将不属于您的API,对吗?然后使用类似查询解释的方法来衡量慢速查询,然后找出哪个字段恰好是慢速。然后,您仅优化慢的事情(例如,参见我的回答here,它优化了单个用例)。如果您是一个大型的ecomerce平台,请不要为此使用Dataloader。构建更智能的东西,不要使用JavaScript。

  • 08-18 02:05