GQLoom

数据加载器(Dataloader)

由于 GraphQL 的灵活性,当我们加载某个对象的关联对象时,我们通常需要执行多个查询。 这就造成了著名的 N+1 查询问题。为了解决这个问题,我们可以使用 DataLoader

DataLoader 能够将多个请求合并为一个请求,从而减少数据库的查询次数,同时还能缓存查询结果,避免重复查询。

示例

表格定义

考虑我们有两张表 usersposts,其中 posts 通过 posts.authorId 关联到 usersid

import {  } from "@gqloom/drizzle"
import {  } from "drizzle-orm"
import * as  from "drizzle-orm/pg-core"

export const  = .("role", ["user", "admin"])

export const  = (
  .("users", {
    : .().(),
    : .().(),
    : .().().(),
    : .(),
    : ().("user"),
  })
)

export const  = (, ({  }) => ({
  : (),
}))

export const  = (
  .("posts", {
    : .().(),
    : .().(),
    : 
      .()
      .()
      .(() => new ()),
    : .().(false),
    : .({ : 255 }).(),
    : .().(),
  })
)

export const  = (, ({  }) => ({
  : (, { : [.], : [.] }),
}))

数据填充

让我们使用 drizzle-seed 为数据库填充一些数据:

import { drizzle } from "drizzle-orm/node-postgres"
import { reset, seed } from "drizzle-seed"
import { config } from "../env.config"
import * as schema from "./schema"

async function main() {
  const db = drizzle(config.databaseUrl, { logger: true })
  await reset(db, schema)
  await seed(db, schema).refine(() => ({
    users: {
      count: 20,
      with: {
        posts: [
          { weight: 0.6, count: [1, 2, 3] },
          { weight: 0.3, count: [5, 6, 7] },
          { weight: 0.1, count: [8, 9, 10] },
        ],
      },
    },
  }))
}

main()

创建解析器

让我们为 User 对象编写一个简单的解析器:

import { , , ,  } from "@gqloom/core"
import { ,  } from "drizzle-orm"
import {  } from "drizzle-orm/node-postgres"
import {  } from "./env.config"
import * as  from "./schema"
import { ,  } from "./schema"

const  = (., { : , : true })

const  = .(, {
  : (.()).(() => .().()),

  : (.())
    .("id")
    .(() => .().().((., .))),
})

export const  = ()

在上面的代码中,我们定义了一个用户解析器,它包含:

  • users 查询:用于获取所有用户
  • posts 字段:用于获取对应用户的所有帖子

下面是一个示例查询,它将返回所有用户的信息以及对应的帖子:

GraphQL Query
query usersWithPosts {
  users {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

这个查询将为每个用户分别查询他们的帖子。我们在前一步在数据库里填充了 20 个用户,所以这个查询将引起 20 次对 posts 表的查询。
这显然是一种非常低效的方式,让我们来看看如何使用 DataLoader 来减少查询次数。

使用 DataLoader

接下来,我们将使用 DataLoader 来优化我们的查询。

import { , , ,  } from "@gqloom/core"
import { ,  } from "drizzle-orm"
import {  } from "drizzle-orm/node-postgres"
import {  } from "./env.config"
import * as  from "./schema"
import { ,  } from "./schema"

const  = (., { : , : true })

const  = .(, {
  : (.()).(() => .().()),

  : (.()) 
    .("id") 
    .(() => .().().((., .))), 

  : (.()) 
    .("id") 
    .(async () => { 
      const  = await  
        .() 
        .() 
        .( 
          ( 
            ., 
            .(() => .) 
          ) 
        ) 
      const  = .(, () => .) 
      return .(() => .(.) ?? []) 
    }), 
})

export const  = ()

在上面的代码中,我们使用 field().load() 来启用数据批量加载,在幕后这将使用 DataLoader 来批量加载数据。
load() 内部,我们通过以下步骤实现数据批量加载:

  1. 使用 in 操作从 posts 表中一次性获取所有当前加载的用户的帖子;
  2. 使用 Map.groupBy() 将帖子列表按作者 ID 分组;
  3. 将用户列表按顺序映射到帖子列表,如果某个用户没有帖子,则返回一个空数组。

如此一来,我们将原先的 20 次查询合并为 1 次查询,从而实现了性能优化。

必须保证查询函数的返回数组顺序与 IDs 数组顺序一致。DataLoader 依赖于此顺序来正确地合并结果。

目录