ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MikroORM 结果缓存(Result Cache)实战指南:机制、配置与自定义适配器

MikroORM 结果缓存(Result Cache)实战指南:机制、配置与自定义适配器 后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载结果缓存是 MikroORM 内置的一套轻量级查询结果缓存机制它允许EntityManager的查询方法与QueryBuilder的查询结果在指定时间内被复用从而避免重复命中数据库。本文以官方文档 docs/docs/caching.md 为骨架结合packages/core与packages/sql中的源码实现和 result-cache 测试套件 的验证行为系统讲解缓存如何开启、缓存键如何生成、命中后如何还原实体以及如何通过实现CacheAdapter接口接入 Redis 等外部存储。结果缓存是什么适用范围与默认行为MikroORM 的结果缓存result cache是一套简单但实用的机制适用于以下EntityManager方法find()findOne()findAndCount()findOneOrFail()count()以及QueryBuilder的所有结果获取方法包括execute()。当这些查询开启缓存后相同条件的查询在缓存有效期内会直接复用上一次的结果不再向数据库发起新的 SQL。关于默认行为官方文档明确指出两点默认使用内存缓存MemoryCacheAdapter该缓存对整个MikroORM实例共享默认过期时间为 1 秒1000ms。需要特别说明的是结果缓存默认是关闭的必须显式开启逐查询或全局配置这与 Identity Map身份映射是两回事——后者始终存在用于保证同一个实体在单位工作Unit of Work内是同一个实例。驱动层接口的注释也明确区分了这一点见 IDatabaseDriver.ts 中cache选项的说明“Result cache is by default disabled, not to be confused with the identity map.”。按查询启用缓存cache选项的三种形态在find()等方法的 options 中传入cache字段即可为该次查询单独开启缓存。cache字段支持三种形态类型定义为boolean | number | [string, number]见 IDatabaseDriver.ts形态含义cache: true使用默认缓存键自动生成和默认过期时间cache: 50使用自动缓存键但将过期时间覆盖为 50mscache: [cache-key, 50]同时指定自定义缓存键和过期时间60s 可写作60_000官方文档给出的find()示例const res await em.find(Book, { author: { name: Jon Snow } }, { populate: [author, tags], cache: 50, // 设置过期时间为 50ms // cache: [cache-key, 50], // 自定义缓存键 过期时间 // cache: true, // 使用默认缓存键和过期时间 });同样的选项也适用于findOne()、findOneOrFail()、findAndCount()与count()例如const res await em.findOneOrFail(Book, { author: { name: Jon Snow } }, { populate: [author, tags], cache: [abc, 100], }); const total await em.count(Book, { author: { name: Jon Snow } }, { cache: 100 });QueryBuilder 的.cache()方法QueryBuilder提供链式方法.cache()默认参数为true同样支持数字与[key, expiration]元组const res await em.createQueryBuilder(Book) .where({ author: { name: Jon Snow } }) .cache() // 使用默认缓存键和过期时间 .getResultList();.cache()的实现位于 packages/sql/src/query/QueryBuilder.ts它只是把配置存入查询状态cache(config: boolean | number | [string, number] true): this { this.ensureNotFinalized(); this.#state.cache config; return this; }getResultList()、getSingleResult()、getCount()、execute()等结果方法在真正执行 SQL 前都会先经过tryCache检查、执行后经storeCache写回见 QueryBuilder.ts因此只要链上调用过.cache()所有结果方法都会生效。全局启用与默认配置resultCache配置详解如果希望对所有查询统一启用缓存可以在MikroORM.init()的配置中设置resultCache字段const orm await MikroORM.init({ resultCache: { // 以下均为默认值 adapter: MemoryCacheAdapter, // 缓存适配器类 expiration: 1000, // 默认过期时间1s options: {}, // 传给适配器构造函数的额外选项 // 也可以全局启用缓存对全部查询生效 // global: 50, // 全局缓存过期时间 50ms }, // ... });resultCache配置项的完整类型定义位于 packages/core/src/utils/Configuration.ts各字段说明如下expiration?: number默认缓存过期时间毫秒默认1000。当某个查询只写了cache: true而未显式指定数字时使用该值。adapter?: { new (...params: any[]): CacheAdapter }缓存适配器类默认MemoryCacheAdapter。需要实现CacheAdapter接口。options?: Dictionary透传给适配器构造函数的选项对象默认{}。例如自定义适配器所需的连接配置、缓存目录等。global?: boolean | number | [string, number]是否对所有查询全局启用结果缓存。可以是true用默认过期时间、一个数字全局过期时间或[key, expiration]元组。开启后即使单个查询没有传cache选项也会生效。全局配置的实际取值逻辑在tryCache/storeCache中体现EntityManager.ts 与 EntityManager.tsconfig ?? this.config.get(resultCache).global;也就是说查询级cache选项优先未指定时回退到全局global配置。全局开关也可以在运行时动态修改测试套件中就有orm.config.get(resultCache).global 100;之后再次置为undefined的用法见 result-cache.postgre.test.ts。适配器实例的创建由Configuration.getResultCacheAdapter()完成Configuration.ts它通过getCachedService按配置惰性创建并缓存单例并将expiration与options合并后传给适配器构造函数getResultCacheAdapter(): CacheAdapter { return this.getCachedService(this.#options.resultCache.adapter!, { expiration: this.#options.resultCache.expiration, ...this.#options.resultCache.options, }); }缓存键是如何生成的理解缓存键的生成规则有助于判断哪些查询会共享缓存、哪些不会。MikroORM 的缓存键生成分两条路径EntityManager系列方法cacheKey()find()等方法的自动缓存键由内部方法cacheKey()生成EntityManager.ts其核心逻辑为剔除与结果无关的选项从 options 中删除ctx、strategy、flushMode、logging、loggerContext、signal、inflightQueryAbortStrategy等字段注释说明 logger context 等可能包含同一查询的动态数据不应参与键计算实体标识优先使用数据库侧信息如果元数据存在使用[schema, tableName, discriminatorValue]表名跨构建、跨进程稳定且为单表继承STI加入判别值元数据未知时才回退到实体类名组合键[entityKey, method, opts, where]其中method是em.find、em.findOne、em.count等RLS 会话上下文作用域如果存在行级安全Row Level Security的会话上下文会将其追加到键尾部避免不同租户/角色之间的缓存串扰。QueryBuilder基于 SQL 与参数QueryBuilder的缓存键由执行路径构造QueryBuilder.tsconst cacheKey: unknown[] [qb.execute, query.sql, query.params, method];即“SQL 语句 绑定参数 结果方法名”的组合同样地存在会话上下文时也会追加到键中。由于键包含完整 SQL 与参数相同查询条件但不同排序、分页的查询不会互相污染。自定义键与会话作用域当使用cache: [cache-key, 50]显式指定键时如果当前存在会话上下文实际存储的键会追加|${JSON.stringify(sessionContext)}后缀见 EntityManager.ts 的注释“a named cache key discards the computed key ... scope it here too”防止 fork 出来的 EntityManager 在相同命名键下串读其他会话的数据。缓存命中后发生了什么命中缓存并不是简单地把 JSON 数据原样返回而是经历了一个实体还原过程。核心逻辑在tryCache()EntityManager.tsconst cached await em.#resultCache.get(cacheKey); if (!cached) { return { key: cacheKey, data: cached }; } // ... if (Array.isArray(cached) merge) { data cached.map(item em.#entityFactory.createT(entityName, item, createOptions)) as unknown as R; } else if (Utils.isObjectEntityDataT(cached) merge) { data em.#entityFactory.createT(entityName, cached, createOptions) as unknown as R; } else { data cached; } await em.#unitOfWork.dispatchOnLoadEvent();关键点对缓存中的实体数据通过实体工厂entityFactory.create()重新创建实体实例并支持merge合并进当前上下文、refresh、schema等选项recomputeSnapshot: true会重算快照还原后还会派发onLoad生命周期事件在find()的调用路径中EntityManager.ts命中缓存后返回实体列表并继续执行entityLoader.populate()完成 populate 关联加载——也就是说缓存命中并不跳过 populate 流程关联数据依然会被正确加载当使用 JOINED 策略时关联数据通常已随查询一并缓存测试中命中后不会再产生新的 SQL。未命中时查询正常执行结果通过storeCache()写回缓存EntityManager.ts过期时间的计算规则为元组取第二个元素、数字取自身、true则为undefined交由适配器使用默认过期时间。清除缓存clearCache()与显式键结果缓存没有自动失效机制除非达到过期时间因此如果需要主动让缓存失效必须为缓存指定显式键之后调用em.clearCache(cacheKey)// 以 book-cache-key 作为缓存键过期时间 60s const res await em.find(Book, { ... }, { cache: [book-cache-key, 60_000] }); // 按名称清除该缓存项 await em.clearCache(book-cache-key);clearCache()的实现EntityManager.ts会先移除指定键如果存在会话上下文还会一并移除${cacheKey}|${JSON.stringify(sessionContext)}变体确保命名键在会话作用域下也能被彻底清理。测试套件对清除行为做了明确验证result-cache.postgre.test.ts在缓存有效期内clearCache(abc)之后再次查询SQL 调用数从 2 增加到 3证明缓存已被清除、查询重新执行。CacheAdapter接口与内置适配器所有缓存后端都必须实现CacheAdapter接口。官方文档给出的接口定义与仓库 packages/core/src/cache/CacheAdapter.ts 中的实现一致export interface CacheAdapter { /** * 获取 name 键下的缓存项。 */ get(name: string): Promiseany; /** * 写入缓存项。origin 用于缓存失效判断应反映数据来源的变化。 */ set(name: string, data: any, origin: string, expiration?: number): Promisevoid; /** * 移除指定缓存项。 */ remove(name: string): Promisevoid; /** * 清空所有缓存项。 */ clear(): Promisevoid; /** * 在 MikroORM.close() 中被调用用于优雅关闭例如 Redis 连接。 */ close?(): Promisevoid; }值得补充的是仓库中实际接口比文档示例更进一步get/set/remove/clear均支持同步与异步两种返回形态并额外定义了用于元数据缓存的同步变体SyncCacheAdapter带可选的combine()方法见 CacheAdapter.ts。适配器同时服务于结果缓存与元数据缓存两条链路。内置适配器一览仓库packages/core/src/cache/目录下提供了四个内置适配器适配器文件用途MemoryCacheAdapterMemoryCacheAdapter.ts结果缓存默认适配器进程内 Map 存储基于时间过期NullCacheAdapterNullCacheAdapter.ts空操作适配器所有读写均为 no-op用于关闭缓存FileCacheAdapterFileCacheAdapter.ts文件落盘缓存JSON 文件主要用于元数据缓存支持combined合并模式GeneratedCacheAdapterGeneratedCacheAdapter.ts基于预生成静态数据的适配器由 CLIcache:generate命令产出MemoryCacheAdapter源码级解读默认的MemoryCacheAdapterMemoryCacheAdapter.ts实现非常简洁值得逐行理解其过期策略export class MemoryCacheAdapter implements CacheAdapter { readonly #data new Mapstring, { data: any; expiration: number }(); readonly #options: { expiration: number }; constructor(options: { expiration: number }) { this.#options options; } getT any(name: string): T | undefined { const data this.#data.get(name); if (data) { if (data.expiration Date.now()) { this.#data.delete(name); // 惰性过期读取时才发现过期并删除 } else { return data.data; } } return undefined; } set(name: string, data: any, origin: string, expiration?: number): void { this.#data.set(name, { data, expiration: Date.now() (expiration ?? this.#options.expiration) }); } }要点底层是Mapstring, { data, expiration }没有后台清理线程采用惰性过期——条目在get()时被检查若已过期则删除并返回undefined视为未命中set()时若无显式expiration回退到构造函数收到的默认过期时间即配置中的resultCache.expiration因为存的是普通对象引用缓存的实体数据与返回结果共享内存这也是为什么tryCache中要重新通过实体工厂创建实例并重算快照。编写自定义适配器如 Redis官方文档特别在close?()的注释中点名了 Redis 场景。要接入外部存储只需实现CacheAdapter接口并在配置中替换adapter即可例如一个基于 Redis 的示意实现import type { CacheAdapter } from mikro-orm/core; class RedisCacheAdapter implements CacheAdapter { constructor(private client: RedisClient, private defaultExpiration: number) {} async get(name: string) { const raw await this.client.get(name); return raw ? JSON.parse(raw) : undefined; } async set(name: string, data: any, origin: string, expiration?: number) { const ttl expiration ?? this.defaultExpiration; await this.client.set(name, JSON.stringify(data), EX, ttl / 1000); } async remove(name: string) { await this.client.del(name); } async clear() { // 按业务规则清空相关键例如 scan del } async close() { await this.client.quit(); // 在 orm.close() 时优雅关闭连接 } } const orm await MikroORM.init({ resultCache: { adapter: RedisCacheAdapter, expiration: 1000, options: { /* 传给构造函数的额外参数 */ }, }, });注意expiration与options会被合并后传入构造函数见前文getResultCacheAdapter()源码因此自定义适配器可以从构造参数中读取默认过期时间。关闭时的清理钩子MikroORM.close()会依次调用元数据缓存适配器与结果缓存适配器的close()MikroORM.tsasync close(force false): Promisevoid { await this.driver.close(force); await this.config.getMetadataCacheAdapter()?.close?.(); await this.config.getResultCacheAdapter()?.close?.(); }close?()是可选的未实现则跳过——这正是为 Redis 这类需要主动断开连接的外部存储准备的优雅关闭入口。测试验证缓存命中、过期与清除的完整证据仓库在 tests/features/result-cache/result-cache.postgre.test.ts 中用 PostgreSQL 环境系统验证了上述全部行为测试手法是通过 mock 查询日志计数来判断是否真的执行了 SQLfind命中与过期L38-L85cache: 100的查询执行 1 次 SQL50ms 后未过期再次查询仍为 1 次命中再推进 1ms100ms 过期后查询 SQL 计数变为 2未命中重新执行。全局缓存L87-L137设置orm.config.get(resultCache).global 100后不带cache选项的查询同样被缓存清除global后恢复原状。findOneOrFail与显式键清除L139-L197使用cache: [abc, 100]命中期间无新 SQL调用em.clearCache(abc)后下一次查询 SQL 计数增加证明显式键被成功清除。count与QueryBuilderL199-L279count支持cache: 100QueryBuilder 链式.cache(100)同样表现为“命中无新 SQL、过期后重新执行”并且.cache()无参数与.cache(100)在键层面是等价的测试最后用.cache()验证过期后重查。元数据未知时的键回退L281-L286未发现元数据的实体缓存键回退为[NotDiscovered, em.find, {}, {}]形式。此外还有 MongoDB 平台的结果缓存测试 result-cache.mongo.test.ts说明该机制对非 SQL 平台同样适用。使用建议与边界结果缓存 ≠ 数据实时性缓存只按过期时间失效写入数据后需要主动clearCache()才能立即可见。因此它更适合低频变化、读多写少的查询如分类列表、统计数据。过期时间以毫秒为单位cache: 50即 50mscache: [book-cache-key, 60_000]即 60s未显式指定时使用resultCache.expiration默认 1000ms。自定义键要能对应到清除动作只有使用显式键的缓存才能通过em.clearCache(key)精准清除自动键由内部规则生成不建议手动猜测。多租户/RLS 场景存在会话上下文时缓存键会自动附加上下文作用域显式键也会被追加后缀避免跨租户串读——清除时clearCache()同样处理了对应变体。与元数据缓存的区别本文介绍的是查询结果缓存实体元数据的磁盘/内存缓存是另一套机制metadataCache配置与FileCacheAdapter等适配器详见 docs/docs/metadata-cache.md。与 Identity Map 的区别结果缓存默认关闭且服务于跨请求复用Identity Map 始终开启且服务于单次单位工作内的实体唯一性二者不要混淆。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐macOS 下 TPM 报 returned 127 怎么解决macOS 下 TPM 报 returned 127 怎么解决 在 macOS 上通过 Homebrew 安装 tmux 并使用 TPMTmux Plu后端MikroORM 结果缓存Result Cache完全指南从默认内存缓存到自定义 CacheAdapterMikroORM 结果缓存Result Cache完全指南从默认内存缓存到自定义 CacheAdapter MikroORM 内置了一套轻量级的结果缓存后端MikroORM 结果缓存实战从 find() 的 cache 选项、全局配置到自定义 CacheAdapter 实现MikroORM 结果缓存实战从 find 的 cache 选项、全局配置到自定义 CacheAdapter 实现 本篇指南基于 MikroORM 官方文档后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表