DataLoader 4大实战模式:每请求创建、双键缓存prime、不可变结果等技巧
【免费下载链接】dataloaderDataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching.项目地址: https://gitcode.com/gh_mirrors/da/dataloader
DataLoader 是一款面向 Node.js 与 JavaScript 的数据加载(Data Loading)通用工具库,它通过"批处理 + 内存缓存"两大核心机制,把应用对数据库、Redis、CouchDB 等后端的零散请求合并成少量批量请求,是 GraphQL 服务与任意数据访问层中的性能利器。本文带你掌握 4 个最实用的 DataLoader 实战模式,以及一份常用选项速查表。
🎯 DataLoader 解决什么问题?
假设一个页面需要展示"我 + 我最好的朋友 + 我的 5 个朋友(含各自的朋友)",朴素写法会发出多达 13 次数据库请求;而接入 DataLoader 后,同样的数据最多 4 次请求,缓存命中时还会更少(详见 README.md 中的 GraphQL 示例)。
它的两大核心机制一句话概括:
| 机制 | 作用 |
|---|---|
| 批处理 Batching | 同一事件循环 tick 内的所有load()请求被合并,一次性传给批量加载函数 |
| 缓存 Caching | 同一 key 只加载一次,后续调用直接命中内存缓存,且与未命中请求同批解析 |
const userLoader = new DataLoader(keys => myBatchGetUsers(keys)); const user = await userLoader.load(1); // 自动合并进同一个批次📌 模式一:每请求创建 DataLoader,杜绝缓存串户
这是官方文档 Common Patterns 推荐的第一原则:在多用户系统中,每个 HTTP 请求开始时新建一组 Loader,请求结束即丢弃。
⚠️ 如果多个用户共用同一个实例,A 用户缓存的数据可能"泄漏"给 B 用户——这是缓存串户事故的头号原因。
推荐用工厂函数把一批 Loader 打包成一个对象,作为rootValue在应用逻辑中传递:
function createLoaders(authToken) { return { users: new DataLoader(ids => genUsers(authToken, ids)), cdnUrls: new DataLoader(urls => genCdnUrls(authToken, urls)), stories: new DataLoader(keys => genStories(authToken, keys)), }; } // 每次收到 web 请求时: const loaders = createLoaders(request.query.authToken);关键点:权限信息(如authToken)在建 Loader 时注入,天然实现了"一人一套缓存"。完整代码见 README.md。
🔁 模式二:用 prime 让双键缓存互相填充
业务中同一实体常有多把"钥匙":用户既能按id查,也能按username查。若两处各自建 Loader,同一用户就会被查两次数据库。
解法:两个 Loader 在批量函数返回结果后,顺手把对方的缓存也"预热"(prime)掉,实现 README.md 所说的"双键缓存":
const userByIDLoader = new DataLoader(async ids => { const users = await genUsersByID(ids); for (const user of users) { usernameLoader.prime(user.username, user); // 顺手填充另一把键的缓存 } return users; });prime(key, value)的三个实用细节(源码实现见 src/index.js):
- 只填不覆盖:key 已存在时不做任何改动;想强制覆盖,先
loader.clear(key).prime(key, value); - 可填 Promise:值可以是 Promise,加载完成时自动解析;
- 可填错误:传入
Error实例可缓存错误,避免反复请求同一个必然失败的 key。
配合cacheKeyFn选项(对象键 → 缓存键),连 Google Datastore 这类复合键也能安全入缓存,参考 examples/GoogleDatastore.md。
🧊 模式三:Object.freeze 强制结果不可变
DataLoader 缓存的值默认被当作只读处理,但库本身不强制——某段代码一旦偷偷改写了缓存对象,所有共享该缓存的调用方都会拿到脏数据。
官方给了一个仅 3 行的高阶函数解法(README.md):
function freezeResults(batchLoader) { return keys => batchLoader(keys).then(values => values.map(Object.freeze)); } const myLoader = new DataLoader(freezeResults(myBatchLoader));任何对冻结对象的赋值在严格模式下都会直接抛错,把"约定"升级为"约束",是多团队协作中的低成本保险。
📦 模式四:包装"返回对象"的批量函数,格式即兼容
DataLoader 严格要求批量函数返回与 keys 等长、按索引对齐的数组;但很多数据库 SDK 返回的是{ key: value }对象或无序结果集。
不必改 SDK,用一个高阶函数做格式适配即可(README.md):
function objResults(batchLoader) { return keys => batchLoader(keys).then(objValues => keys.map(key => objValues[key] || new Error(`No value for ${key}`)) ); }各后端的现成参考实现都在 examples/ 目录:
| 后端 | 批量机制 | 示例文档 |
|---|---|---|
| SQL(SQLite/通用) | WHERE id IN (...) | examples/SQL.md |
| Knex.js(PostgreSQL/MySQL) | whereIn查询构造器 | examples/Knex.md |
| Redis | MGET批量命令 | examples/Redis.md |
| CouchDB | HTTP 批量文档 API | examples/CouchDB.md |
| Google Datastore | datastore.get(keys) | examples/GoogleDatastore.md |
| RethinkDB | getAll(注意结果无序的坑) | examples/RethinkDB.md |
⚡ 常用选项速查表
来自 README.md API 章节,覆盖绝大多数定制需求:
| 选项 | 默认值 | 何时使用 |
|---|---|---|
cache | true | 设为false关闭缓存(注意:批量 keys 会含重复项!) |
maxBatchSize | Infinity | 限制单批 key 数量,防 SQL 语句过长 |
cacheKeyFn | key => key | 对象键需要转字符串作缓存键时 |
cacheMap | new Map() | 长生命周期 Loader 换成 LRU 缓存,防内存膨胀 |
batchScheduleFn | 立即调度 | 自定义调度窗口,如 100ms 内合并请求 |
name | null | 给 APM 监控工具标记 Loader 名称 |
缓存与批处理的协作细节(缓存项仍会等待当前批次完成、错误缓存策略、clear/clearAll的时机)值得一读,见 README.md Caching 章节;核心 API(load/loadMany/clear/prime)的 TypeScript 定义在 src/index.d.ts,行为测试用例可参考 src/tests/dataloader.test.js。
✅ 总结:4 大模式一页带走
- 每请求创建:请求开始新建 Loader 工厂、结束即弃,权限随实例隔离,防缓存串户;
- 双键 prime:多个入口键共享实体时,用
prime让各 Loader 缓存互相预热; - 冻结不可变:
freezeResults包装一层Object.freeze,把缓存约定变成运行时约束; - 对象结果适配:
objResults高阶函数抹平 SDK 返回格式差异,让任意批量 API 即插即用。
掌握这 4 个模式,你就能安全、高效地把 DataLoader 嵌入任何数据访问层——批量请求下降一个数量级,而应用代码依然保持"按单个 key 加载"的简洁写法。
【免费下载链接】dataloaderDataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching.项目地址: https://gitcode.com/gh_mirrors/da/dataloader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考