MikroORM 内部架构深度解析:Data Mapper、Unit of Work 与 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点击查看免费下载导读MikroORM 是面向 Node.js 的 TypeScript ORM基于 Data Mapper、Unit of Work 与 Identity Map 三大经典企业应用架构模式构建。本文以官方 架构总览文档 为骨架结合packages/core/src下的真实源码实现深入拆解 EntityManager、UnitOfWork、IdentityMap、Hydrator、Driver、QueryBuilder 等核心组件的职责与调用链并完整还原实体生命周期、水合hydration、变更跟踪flush、关联加载、序列化、事件系统等关键机制。读完本文你将理解 MikroORM 为何要求每个请求 fork 一个 EntityManager以及查询结果为何能保持同一行数据始终是同一个对象引用从而能在生产环境中正确使用事务、并发与请求上下文。三大核心模式MikroORM 的架构基石MikroORM 实现了 Martin Fowler《企业应用架构模式》Patterns of Enterprise Application Architecture中三个广为人知的模式它们是理解整个 ORM 行为的前提Data Mapper数据映射器实体是纯粹的普通对象plain objects对数据库一无所知所有持久化逻辑由 ORM 承担。实体不需要继承任何基类除非你主动使用BaseEntity也不需要感知 SQL、连接或事务。Unit of Work工作单元跟踪一个请求期间对实体所做的全部变更并在单个事务中一次性持久化。这使得修改多个实体后调用一次flush()成为可能。Identity Map标识映射保证在一个请求上下文中每一行数据库记录恰好对应一个实体实例。重复查询同一主键时返回的是同一个对象引用这是实体身份一致性的来源。从源码结构看这三个模式的实现集中在 packages/core/src/unit-of-work/IdentityMap.ts 与 packages/core/src/unit-of-work/UnitOfWork.tsIdentityMap内部以MapEntityCtor, Mapstring, AnyEntity的嵌套结构按实体类分桶存储并以序列化主键哈希schema : hash作为键见getPkHash()IdentityMap.ts它还支持**备用键alternate key**查找通过非主键的唯一属性也能命中缓存并在删除实体时清理这些备用键条目见storeByKey()IdentityMap.ts。关键组件一览组件职责EntityManager所有 ORM 操作的门面facade提供find、persist、remove、flush等方法也是请求上下文的入口。UnitOfWork跟踪实体变更、计算变更集change sets、对查询排序、管理事务。IdentityMap按主键缓存实体实例保证一行记录只对应一个实例。MetadataStorage持有启动时发现discovery得到的实体定义属性、关系、索引等。Hydrator将数据库行转换为实体实例。Driver抽象数据库特定操作SQL 驱动 vs MongoDB 驱动。QueryBuilder以编程方式构建并执行查询仅 SQL 驱动可用。这些组件的实现分布于 packages/core/src/EntityManager.ts、packages/core/src/metadata/MetadataStorage.ts、packages/core/src/hydration/Hydrator.ts 与 packages/core/src/drivers/DatabaseDriver.ts。其中 Hydrator 有两个具体实现ObjectHydrator与EntityFactory内的内联水合逻辑MongoDB 驱动默认走独立的ObjectHydrator见 packages/core/src/hydration/ObjectHydrator.ts。有状态设计与请求上下文为什么要 forkMikroORM 是**有状态stateful**的。EntityManager 持有的 IdentityMap 会在其整个生命周期内持续累积实体。这是刻意设计——只有累积状态才能实现变更追踪和实体身份一致性。但这同时意味着绝不能跨请求共享同一个 EntityManager 实例否则会内存无界增长IdentityMap 永不释放实体读到陈旧数据上次请求遗留的脏实体并发请求之间产生竞态条件同一实体被多个请求同时修改。解决方案是为每个请求 fork 一个 EntityManager// 在中间件或请求处理器中 const em orm.em.fork();从源码看fork()的关键行为见 EntityManager.ts默认clear: true清空父 EM 的 IdentityMap 与持久化栈共享同一个driver与metadata并复制过滤器filters、会话上下文、schema 等配置若传入clear: false则会把父 EM 中已管理的实体和 persist 栈逐一手动注册进 fork用于需要继承上下文的场景。ForkOptions还支持flushMode、disableTransactions、keepTransactionContext、freshEventManager全新的 EventManager与cloneEventManager克隆当前监听器等开关。RequestContext基于 AsyncLocalStorage 的自动 fork为方便起见MikroORM 提供RequestContext辅助类基于 Node.jsAsyncLocalStorage自动提供请求作用域的 EntityManager 实例app.use((req, res, next) { RequestContext.create(orm.em, next); }); // 后续代码中 - 自动使用 fork 出来的 EM const users await orm.em.find(User, {});RequestContext的实现见 packages/core/src/utils/RequestContext.tscreate()使用AsyncLocalStorage.run()适合 express 风格、带next回调的中间件而enter()使用AsyncLocalStorage.enterWith()适合 elysia 风格、无next回调的中间件。createContext()内部对传入的 EM可以是单个或数组对应多数据库场景执行em.fork({ useContext: true, ...options })并将 fork 存入以 EM 名称默认default为键的 MapRequestContext.getEntityManager(name)可在任意异步上下文中取回对应的 fork。还可通过CreateContextOptions继承自ForkOptions在创建上下文时全局覆盖 fork 行为。完整细节参见 Identity Map 与请求上下文。实体生命周期New / Managed / Detached / Removed实体相对 EntityManager 而言始终处于以下几种状态之一状态说明New通过em.create()或构造函数创建的实体将在下次flush()时被 INSERT。Managed实体被 UnitOfWork 跟踪变更会在flush()时被检测并持久化。实体从数据库加载后、或经flush()插入后进入该状态。Detached实体不被任何 UnitOfWork 跟踪。可能是通过em.clear()显式分离或它属于另一个 EntityManager fork。用em.merge()重新挂接。Removed通过em.remove()排定删除的实体将在下次flush()时被 DELETE。在源码层Managed 的判定依据是__managed标志与__originalEntityData快照。UnitOfWork.register()会存储实体到 IdentityMap、标记__managed true并在必要时写入原始数据快照UnitOfWork.tsmerge()则负责把外部实体重新挂进当前 EM级联cascade处理关联实体并利用EntityComparator.prepareEntity()重建快照UnitOfWork.ts。unsetIdentity()展示了 Detached 的底层细节——不仅从 IdentityMap 删除实体还会遍历所有引用它的已管理实体并清理关联引用避免后续 flush 时被重新插入UnitOfWork.ts。从查询到实体水合Hydration当你查询数据库时MikroORM 通过**水合hydration**过程把原始行转换为实体实例水合的关键点先查 IdentityMap创建新实例前ORM 先检查该主键对应的实体是否已存在于 IdentityMap 中。单实例保证在一个请求上下文内同一数据库行始终返回同一个对象引用。关系引用关联实体最初只以**引用reference**形式加载——即只含主键的对象只有在 populate 时才会被完整加载。状态快照水合后的状态会被内部保存供后续变更检测使用。底层实现上EntityFactory.create()是水合的入口EntityFactory.ts它会先解包Reference、处理鉴别器列processDiscriminatorColumn用于 STI 单表继承、通过findEntity()在 IdentityMap 中查找已存在实例若命中且无需refresh则直接复用现有实例否则新建并水合。FactoryOptions中initialized、newEntity、merge、refresh、convertCustomTypes、recomputeSnapshot等选项控制水合的不同侧面。从实体到数据库快照式变更追踪与 FlushMikroORM 采用**基于快照snapshot-based**的变更追踪。实体被水合或持久化时ORM 保存其状态的副本在flush()时将当前状态与该快照比对flush 操作计算变更集将当前实体状态与保存的快照比对。对查询排序使用拓扑排序CommitOrderCalculator尊重外键约束。批量操作将 INSERT、UPDATE、DELETE 分组以提升效率。包裹事务所有变更原子提交。更新快照提交成功后快照更新以反映新状态。源码层面的UnitOfWork.commit()UnitOfWork.ts展示了几个值得注意的工程细节通过insideFlush一个AsyncLocalStorage上下文防止在 flush 钩子内重复提交并维护一个#flushQueue让 flush 期间再次触发的提交排队串行执行避免Promise.all并发提交导致的验证错误doCommit()UnitOfWork.ts依次派发beforeFlush、onFlush、afterFlush事件若变更集、集合更新与额外更新均为空则直接返回、不开启事务否则根据implicitTransactions配置决定是否用transactional()包裹persistToDatabase()并支持传入ctx复用外层事务上下文与TransactionEventBroadcastercomputeChangeSets()UnitOfWork.ts遍历移除栈、IdentityMap 与持久化栈级联计算变更集并把删除后又以相同主键重新创建的情况识别为DELETE_EARLY以正确处理实体重建场景。FlushMode枚举定义了 flush 的触发时机packages/core/src/enums.ts模式行为COMMIT commitEM 延迟 flush直到当前事务提交时。AUTO auto默认模式仅在必要时才 flush。ALWAYS always每次查询前都 flush。关于 flush 模式与事务细节参见 Unit of Work 与 事务。加载关联Reference 与 populate关系不会自动加载。默认情况下关系属性只包含一个引用——只含主键、不含其他数据的对象const book await em.findOne(Book, 1); console.log(book.author); // Reference: { id: 5 } console.log(book.author.name); // undefined - not loaded!要加载关联实体使用populate选项const book await em.findOne(Book, 1, { populate: [author] }); console.log(book.author.name); // John Doe - loaded!三种加载策略MikroORM 支持三种加载策略策略说明适用场景select-in按关系层级逐层发起独立查询使用IN子句一对多关系避免笛卡尔积爆炸joined单条带 JOIN 的查询多对一关系或需要基于关联列过滤时balanced默认多对一用joined一对多用select-in通用场景兼顾两者优点// 使用特定策略 const books await em.find(Book, {}, { populate: [author, tags], strategy: LoadStrategy.JOINED, });从源码看EntityLoader在构建 populate 计划时会根据策略为每个关联属性选择实现路径select-in策略为嵌套关联补充按主键集合批量加载的字段EntityLoader.ts并针对自引用关系在joined与select-in之间做权衡EntityLoader.ts即使不显式传策略populate: [*]这类通配场景也会退回select-in以规避 JOIN 笛卡尔积EntityLoader.ts。详细的策略对比见 Loading Strategiespopulate 的完整用法见 Populating Relations。QueryBuilder原始数据 vs 水合实体QueryBuilder仅 SQL 驱动提供两种获取结果的方式qb.execute()—— 原始数据直接返回驱动给出的普通 JavaScript 对象不经过 IdentityMap 与水合const rows await em.createQueryBuilder(User) .select([id, name]) .where({ active: true }) .execute(); // rows [{ id: 1, name: John }, { id: 2, name: Jane }] // 这些是普通对象不是实体实例qb.getResult()—— 水合实体返回完整水合的实体实例并注册进 IdentityMapconst users await em.createQueryBuilder(User) .select(*) .where({ active: true }) .getResult(); // users [User { id: 1, name: John }, User { id: 2, name: Jane }] // 这些是被管理的实体变更会被追踪何时用哪种只读查询、不需要变更追踪时用execute()需要把结果当作实体继续操作时用getResult()。从EntityManager.ts源码看em.createQueryBuilder()在事务内等场景还会自动fork({ keepTransactionContext: true })以保证查询与当前事务共享上下文EntityManager.ts。QueryBuilder 完整文档见 QueryBuilder。序列化实体到普通对象的两种路径MikroORM 提供两种将实体转换为普通对象的方式隐式序列化对实体调用toJSON()或toObject()时序列化由加载时的populate提示驱动const user await em.findOne(User, 1, { populate: [books], fields: [name, books.title], }); const dto wrap(user).toObject(); // 只包含: id, name, books[].id, books[].title关键行为只有被 populate 的关系才会序列化为对象未 populate 的关系序列化为外键值fields选项控制输出中出现哪些属性。显式序列化需要完全控制时使用serialize()辅助函数import { serialize } from mikro-orm/core; const dto serialize(user, { populate: [books, profile], exclude: [password], forceObject: true, });这会忽略原有的 populate 提示让你精确指定包含与排除的内容。序列化器的实现位于 packages/core/src/serialization/EntitySerializer.ts 与 packages/core/src/serialization/EntityTransformer.ts。所有选项包括序列化分组见 Serializing。驱动架构一套核心多数据库适配MikroORM 使用驱动driver抽象支持多种数据库。mikro-orm/core包承载与数据库无关的逻辑EntityManager、UnitOfWork、IdentityMap各驱动包提供数据库特定的实现。SQL 驱动mikro-orm/postgresql、mikro-orm/pglite、mikro-orm/mysql、mikro-orm/mariadb、mikro-orm/sqlite、mikro-orm/libsql、mikro-orm/sql-js、mikro-orm/mssql、mikro-orm/oracledb—— 全部支持完整 QueryBuilder。MongoDB 驱动mikro-orm/mongodb—— 使用原生 MongoDB 驱动没有 QueryBuilder改用em.find()配合过滤器对象。功能SQL 驱动MongoDB 驱动QueryBuilder完整支持不可用事务ACID 事务MongoDB 事务4.0关系外键、JOIN引用无 JOIN迁移Schema diff不需要无 schemaM:N 关系中间表拥有方侧引用数组驱动抽象的核心是 packages/core/src/drivers/IDatabaseDriver.ts 与 packages/core/src/drivers/DatabaseDriver.ts平台差异如$ilike、$overlap等 PostgreSQL 专属操作符见 packages/core/src/enums.ts由各驱动包内的 Platform 实现处理。绝大多数 MikroORM 功能在所有驱动上行为一致。主要差异在于MongoDB 没有 JOIN 支持因此不支持按关联实体的属性过滤——你需要从拥有侧查询或对数据做反规范化。事件系统实体生命周期的钩子MikroORM 在实体生命周期关键节点触发事件。可用事件实体级onInit、onLoad、beforeCreate、afterCreate、beforeUpdate、afterUpdate、beforeDelete、afterDelete、beforeUpsert、afterUpsertFlush 级beforeFlush、onFlush、afterFlush事务级beforeTransactionStart、afterTransactionStart、beforeTransactionCommit、afterTransactionCommit、beforeTransactionRollback、afterTransactionRollback事件可通过生命周期钩子实体方法上的装饰器或事件订阅者独立类处理Entity() class User { BeforeCreate() setCreatedAt() { this.createdAt new Date(); } } // 或通过订阅者 class UserSubscriber implements EventSubscriberUser { getSubscribedEntities() { return [User]; } beforeCreate(args: EventArgsUser) { args.entity.createdAt new Date(); } }事件分发的核心是 packages/core/src/events/EventManager.ts 与 packages/core/src/events/EventSubscriber.ts事务事件由 packages/core/src/events/TransactionEventBroadcaster.ts 在事务开始/提交/回滚时广播。UnitOfWork.dispatchOnLoadEvent()UnitOfWork.ts展示了onLoad事件的触发时机——实体加载后、且存在对应监听器时才派发并用__onLoadFired防止重复触发。完整事件参考见 Events and Hooks。延伸阅读Entity Manager —— EntityManager API 使用指南Unit of Work —— 变更追踪与 flush 模式Identity Map —— 请求上下文与 forkPopulating Relations —— 加载关联实体Loading Strategies —— joined 与 select-in 策略对比Serializing —— 实体转 DTOEvents and Hooks —— 生命周期事件Transactions —— 事务管理赞分享后端【免费下载链接】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 7 架构深度解析Data Mapper、Identity Map 与 Unit of Work 的完整实现链路MikroORM 7 架构深度解析Data Mapper、Identity Map 与 Unit of Work 的完整实现链路 本文基于 MikroORM后端MikroORM 入门指南基于 Data Mapper、Unit of Work 与 Identity Map 的 TypeScript ORMMikroORM 入门指南基于 Data Mapper、Unit of Work 与 Identity Map 的 TypeScript ORM MikroO后端MikroORM 实战指南基于 Data Mapper、Unit of Work 与 Identity Map 的 TypeScript ORM 快速上手MikroORM 实战指南基于 Data Mapper、Unit of Work 与 Identity Map 的 TypeScript ORM 快速上手 M后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考