使用 DataLoader 缓存数据

缓存是设计可扩展和高性能 GraphQL API 的重要组成部分。对于 GraphQL API,我们可能希望缓存来自慢速外部 API 的字段值。缓存的目标是在最需要的地方提高性能。

使用 DataLoader 缓存数据

缓存是设计可扩展和高性能 GraphQL API 的重要组成部分。

但是,什么是缓存?缓存是一种技术设计,旨在通过将结果临时保存在缓存中来避免计算密集型任务。缓存是重要数据在内存中的一个位置,它可以比常规过程更快地传递给客户端。

例如,对于 GraphQL API,我们可能希望缓存来自慢速外部 API 的字段值。缓存的目标是在最需要的地方提高性能。

缓存如何更快?


对计算密集型数据使用缓存会更快,原因如下:

  • 缓存通常不存储在存储中,而是存储在内存中,这要快得多。
  • 缓存比数据库小,因此要过滤的值要少得多。
  • 虽然某些缓存数据可能会被查询很多次,但它只会从数据库中提取一次,因此多次提取所浪费的时间更少。

我们如何使用缓存?


使用 DataLoader 进行缓存


我首选的缓存平台是 dataloader,在本教程中,我将解释如何将 dataloader 与 Node.js 和 Typescript 一起使用,以便在您的服务器中进行缓存。

dataloader 包是专门为 GraphQL 创建的,但它可以与其他 Node.js 程序一起使用。

使用 DataLoader


1.在你的项目中安装dataloader


通过运行以下命令安装数据加载器:

npm install --save dataloader 或 yarn add dataloader

2.导入数据加载器


首先,导入 dataloader 和 LRU(LRU 是一种缓存算法,它只会将最多 X 个请求的项目放入缓存中)。

import DataLoader from 'dataloader'
import { LRUMap } from 'lru_map'

3. 定义将被保存到我们的缓存的类型


第一种应该是一种表示数据的方式,比如带有名字的字符串,另一种是数据本身。例如,假设我们正在尝试缓存书籍。我们的包名称将是书籍 ID,因此我们可以分辨出哪本书是哪本书。我们的包裹数据将是书籍长度、作者、价格等...

所以在这种情况下,我们的模式看起来像这样:

type book {
  id: ID!
  name: String!
  author: author!
  price: Int!
  description: String!
  length: Int!
}
type author {
  id: ID!
  name: String!
}

相应的数据类型如下所示:

type packageId = string
type PackageData = /* note that this is the type of the data we represent. */ {
  id: string
  name: string
  author: {
    id: string
    name: string
  }
  price: number
  description: string
  length: number
}

我们编写类型的原因是为了类型安全,假设我们有具有不同值的书籍和玩具,我们不希望以某种方式将玩具插入到我们的书籍缓存中

4.创建数据加载器实例

const dataLoader = new DataLoader<packageId, PackageData>(
  async (/* (1) */keys: readonly packageId[]) => { /* (2) */
    return await Promise.all(
      keys.map((packageId) => {
        await getData(packageId).catch((e) => e);
      })
    );
  },{
    cacheMap: new LRUMap(100); /* (3) */
  }
);

如您所见,我们将用户要求的密钥提供给数据加载器 (1)。

我建议将键设置为某种 id,原因如下:假设在您的 api 中,用户使用其 id 请求数据,因此用户将使用 id:"something" 获取数据,您可以只传递 id作为关键,而不是改变它。

数据加载器将首先检查它是否有缓存中的键,如果有,它将返回数据,而不通过数据库,为您节省大量宝贵的时间。

在那之后(2),我们给数据加载器一个获取数据的函数,以防它在缓存中没有它。在这种情况下,我有函数 getData,并且我正在使用键从我的数据库中“获取数据”。

最后(3)我们给它cacheMap和一些值,该值表示dataloader将缓存多少个查询,在这种情况下,在100个值之后,它将删除最少使用的值(未被查询的那个)最长的时间)为第 101 个值腾出空间。

从现在开始,要查询数据,您只需运行

dataLoader.load(keys)

在 GraphQL 中 使用 Dataloader


Dataloader 旨在与 GraphQL 一起使用,以解决 n+1 问题

N+1 问题是设计 GraphQL API 时的常见问题。查看下面的查询,我们可以看到 GraphQL API 将调用 20 次 Book.author 解析器:

query BooksWithAuthors {
  books(first: 20) {
    id
    title
    author {
      id
      name
    }
  }
}

根据解析器的实现,此查询可能会触发 20 个 SQL 查询或 API 调用来解析可能写过多本书的作者。 Dataloader 通过缓存、延迟和分组类似的解析器调用来帮助解决这个问题。

如何在 GraphQL 中使用 dataloader 包?


要将数据加载器与 GraphQL 一起使用,只需在上下文中传递它!

现在,您的代码应该看起来像这样:

import DataLoader from "dataloader";
import { LRUMap } from "lru_map";

type packageId = string;
type PackageData = Package;

export type GraphQLContext = {
  dataLoader: DataLoader<packageId, PackageData>;
  ...
};
const dataLoader = new DataLoader<packageId, PackageData>(
  async (keys: readonly PackageId[]) => {
    return await Promise.all(
      keys.map((packageId) => {
        await getData(packageId).catch((e) => e);
      })
    );
  },
  {
    cacheMap: new LRUMap(100),
  }
);
...
export async function contextFactory() {
  return { dataLoader, ... };
}

现在,在您的解析器中,只需调用 context.dataLoader.load(keys) 就可以了!您现在在您的服务器中有缓存!

解析器实现的示例:

export const resolvers = {
  Query: {
    getBook: async (parent, input, context) => {
      return context.dataLoader.load(input.bookId)
    }
  }
}