使用Facebook的DataLoader传递参数

时间:2017-09-21 19:51:23

标签: javascript graphql graphql-js

我正在使用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));};

但是正如你所看到的那样,它很黑,而且感觉不太好......

有谁知道如何实现这一目标?

1 个答案:

答案 0 :(得分:1)

我不确定这个问题是否有一个很好的答案,仅仅是因为没有为该用例创建Dataloader,而是我与Dataloader进行了广泛的合作,编写了类似的实现并在其他编程语言上探讨了类似的概念。

让我们理解为什么不为该用例创建Dataloader,以及我们如何使它能够正常工作(大致与您的示例类似)。

Dataloader并非用于获取字段的子集

数据加载器用于简单的键值查找。这意味着给定 key 这样的ID,它将在其后加载一个值。为此,它假定ID后面的对象在失效之前始终是相同的。这是启用数据加载器功能的单一假设。没有它,Dataloader的三个关键功能将不再起作用:

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

如果要最大化Dataloader的功能,这将导致以下两个重要规则:

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

同一实体应该始终具有相同的密钥。听起来似乎微不足道,但实际上不在示例中。标识为1且字段为idname的用户应与标识为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的自定义地图实现。足够了解我们的缓存这些知识就足够了。

结论

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

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