部架構(gòu)深度解析:Data Mapper、Unit of Work 與 Identity Map 的協(xié)同工作方式)
后端【免費(fèi)下載鏈接】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點(diǎn)擊查看免費(fèi)下載導(dǎo)讀MikroORM 是面向 Node.js 的 TypeScript ORM基于 Data Mapper、Unit of Work 與 Identity Map 三大經(jīng)典企業(yè)應(yīng)用架構(gòu)模式構(gòu)建。本文以官方 架構(gòu)總覽文檔 為骨架結(jié)合packages/core/src下的真實源碼實現(xiàn)深入拆解 EntityManager、UnitOfWork、IdentityMap、Hydrator、Driver、QueryBuilder 等核心組件的職責(zé)與調(diào)用鏈并完整還原實體生命周期、水合hydration、變更跟蹤flush、關(guān)聯(lián)加載、序列化、事件系統(tǒng)等關(guān)鍵機(jī)制。讀完本文你將理解 MikroORM 為何要求每個請求 fork 一個 EntityManager以及查詢結(jié)果為何能保持同一行數(shù)據(jù)始終是同一個對象引用從而能在生產(chǎn)環(huán)境中正確使用事務(wù)、并發(fā)與請求上下文。三大核心模式MikroORM 的架構(gòu)基石MikroORM 實現(xiàn)了 Martin Fowler《企業(yè)應(yīng)用架構(gòu)模式》Patterns of Enterprise Application Architecture中三個廣為人知的模式它們是理解整個 ORM 行為的前提Data Mapper數(shù)據(jù)映射器實體是純粹的普通對象plain objects對數(shù)據(jù)庫一無所知所有持久化邏輯由 ORM 承擔(dān)。實體不需要繼承任何基類除非你主動使用BaseEntity也不需要感知 SQL、連接或事務(wù)。Unit of Work工作單元跟蹤一個請求期間對實體所做的全部變更并在單個事務(wù)中一次性持久化。這使得修改多個實體后調(diào)用一次flush()成為可能。Identity Map標(biāo)識映射保證在一個請求上下文中每一行數(shù)據(jù)庫記錄恰好對應(yīng)一個實體實例。重復(fù)查詢同一主鍵時返回的是同一個對象引用這是實體身份一致性的來源。從源碼結(jié)構(gòu)看這三個模式的實現(xiàn)集中在 packages/core/src/unit-of-work/IdentityMap.ts 與 packages/core/src/unit-of-work/UnitOfWork.tsIdentityMap內(nèi)部以MapEntityCtor, Mapstring, AnyEntity的嵌套結(jié)構(gòu)按實體類分桶存儲并以序列化主鍵哈希schema : hash作為鍵見getPkHash()IdentityMap.ts它還支持**備用鍵alternate key**查找通過非主鍵的唯一屬性也能命中緩存并在刪除實體時清理這些備用鍵條目見storeByKey()IdentityMap.ts。關(guān)鍵組件一覽組件職責(zé)EntityManager所有 ORM 操作的門面facade提供find、persist、remove、flush等方法也是請求上下文的入口。UnitOfWork跟蹤實體變更、計算變更集change sets、對查詢排序、管理事務(wù)。IdentityMap按主鍵緩存實體實例保證一行記錄只對應(yīng)一個實例。MetadataStorage持有啟動時發(fā)現(xiàn)discovery得到的實體定義屬性、關(guān)系、索引等。Hydrator將數(shù)據(jù)庫行轉(zhuǎn)換為實體實例。Driver抽象數(shù)據(jù)庫特定操作SQL 驅(qū)動 vs MongoDB 驅(qū)動。QueryBuilder以編程方式構(gòu)建并執(zhí)行查詢僅 SQL 驅(qū)動可用。這些組件的實現(xiàn)分布于 packages/core/src/EntityManager.ts、packages/core/src/metadata/MetadataStorage.ts、packages/core/src/hydration/Hydrator.ts 與 packages/core/src/drivers/DatabaseDriver.ts。其中 Hydrator 有兩個具體實現(xiàn)ObjectHydrator與EntityFactory內(nèi)的內(nèi)聯(lián)水合邏輯MongoDB 驅(qū)動默認(rèn)走獨(dú)立的ObjectHydrator見 packages/core/src/hydration/ObjectHydrator.ts。有狀態(tài)設(shè)計與請求上下文為什么要 forkMikroORM 是**有狀態(tài)stateful**的。EntityManager 持有的 IdentityMap 會在其整個生命周期內(nèi)持續(xù)累積實體。這是刻意設(shè)計——只有累積狀態(tài)才能實現(xiàn)變更追蹤和實體身份一致性。但這同時意味著絕不能跨請求共享同一個 EntityManager 實例否則會內(nèi)存無界增長IdentityMap 永不釋放實體讀到陳舊數(shù)據(jù)上次請求遺留的臟實體并發(fā)請求之間產(chǎn)生競態(tài)條件同一實體被多個請求同時修改。解決方案是為每個請求 fork 一個 EntityManager// 在中間件或請求處理器中 const em orm.em.fork();從源碼看fork()的關(guān)鍵行為見 EntityManager.ts默認(rèn)clear: true清空父 EM 的 IdentityMap 與持久化棧共享同一個driver與metadata并復(fù)制過濾器filters、會話上下文、schema 等配置若傳入clear: false則會把父 EM 中已管理的實體和 persist 棧逐一手動注冊進(jìn) fork用于需要繼承上下文的場景。ForkOptions還支持flushMode、disableTransactions、keepTransactionContext、freshEventManager全新的 EventManager與cloneEventManager克隆當(dāng)前監(jiān)聽器等開關(guān)。RequestContext基于 AsyncLocalStorage 的自動 fork為方便起見MikroORM 提供RequestContext輔助類基于 Node.jsAsyncLocalStorage自動提供請求作用域的 EntityManager 實例app.use((req, res, next) { RequestContext.create(orm.em, next); }); // 后續(xù)代碼中 - 自動使用 fork 出來的 EM const users await orm.em.find(User, {});RequestContext的實現(xiàn)見 packages/core/src/utils/RequestContext.tscreate()使用AsyncLocalStorage.run()適合 express 風(fēng)格、帶next回調(diào)的中間件而enter()使用AsyncLocalStorage.enterWith()適合 elysia 風(fēng)格、無next回調(diào)的中間件。createContext()內(nèi)部對傳入的 EM可以是單個或數(shù)組對應(yīng)多數(shù)據(jù)庫場景執(zhí)行em.fork({ useContext: true, ...options })并將 fork 存入以 EM 名稱默認(rèn)default為鍵的 MapRequestContext.getEntityManager(name)可在任意異步上下文中取回對應(yīng)的 fork。還可通過CreateContextOptions繼承自ForkOptions在創(chuàng)建上下文時全局覆蓋 fork 行為。完整細(xì)節(jié)參見 Identity Map 與請求上下文。實體生命周期New / Managed / Detached / Removed實體相對 EntityManager 而言始終處于以下幾種狀態(tài)之一狀態(tài)說明New通過em.create()或構(gòu)造函數(shù)創(chuàng)建的實體將在下次flush()時被 INSERT。Managed實體被 UnitOfWork 跟蹤變更會在flush()時被檢測并持久化。實體從數(shù)據(jù)庫加載后、或經(jīng)flush()插入后進(jìn)入該狀態(tài)。Detached實體不被任何 UnitOfWork 跟蹤??赡苁峭ㄟ^em.clear()顯式分離或它屬于另一個 EntityManager fork。用em.merge()重新掛接。Removed通過em.remove()排定刪除的實體將在下次flush()時被 DELETE。在源碼層Managed 的判定依據(jù)是__managed標(biāo)志與__originalEntityData快照。UnitOfWork.register()會存儲實體到 IdentityMap、標(biāo)記__managed true并在必要時寫入原始數(shù)據(jù)快照UnitOfWork.tsmerge()則負(fù)責(zé)把外部實體重新掛進(jìn)當(dāng)前 EM級聯(lián)cascade處理關(guān)聯(lián)實體并利用EntityComparator.prepareEntity()重建快照UnitOfWork.ts。unsetIdentity()展示了 Detached 的底層細(xì)節(jié)——不僅從 IdentityMap 刪除實體還會遍歷所有引用它的已管理實體并清理關(guān)聯(lián)引用避免后續(xù) flush 時被重新插入UnitOfWork.ts。從查詢到實體水合Hydration當(dāng)你查詢數(shù)據(jù)庫時MikroORM 通過**水合hydration**過程把原始行轉(zhuǎn)換為實體實例水合的關(guān)鍵點(diǎn)先查 IdentityMap創(chuàng)建新實例前ORM 先檢查該主鍵對應(yīng)的實體是否已存在于 IdentityMap 中。單實例保證在一個請求上下文內(nèi)同一數(shù)據(jù)庫行始終返回同一個對象引用。關(guān)系引用關(guān)聯(lián)實體最初只以**引用reference**形式加載——即只含主鍵的對象只有在 populate 時才會被完整加載。狀態(tài)快照水合后的狀態(tài)會被內(nèi)部保存供后續(xù)變更檢測使用。底層實現(xiàn)上EntityFactory.create()是水合的入口EntityFactory.ts它會先解包Reference、處理鑒別器列processDiscriminatorColumn用于 STI 單表繼承、通過findEntity()在 IdentityMap 中查找已存在實例若命中且無需refresh則直接復(fù)用現(xiàn)有實例否則新建并水合。FactoryOptions中initialized、newEntity、merge、refresh、convertCustomTypes、recomputeSnapshot等選項控制水合的不同側(cè)面。從實體到數(shù)據(jù)庫快照式變更追蹤與 FlushMikroORM 采用**基于快照snapshot-based**的變更追蹤。實體被水合或持久化時ORM 保存其狀態(tài)的副本在flush()時將當(dāng)前狀態(tài)與該快照比對flush 操作計算變更集將當(dāng)前實體狀態(tài)與保存的快照比對。對查詢排序使用拓?fù)渑判駽ommitOrderCalculator尊重外鍵約束。批量操作將 INSERT、UPDATE、DELETE 分組以提升效率。包裹事務(wù)所有變更原子提交。更新快照提交成功后快照更新以反映新狀態(tài)。源碼層面的UnitOfWork.commit()UnitOfWork.ts展示了幾個值得注意的工程細(xì)節(jié)通過insideFlush一個AsyncLocalStorage上下文防止在 flush 鉤子內(nèi)重復(fù)提交并維護(hù)一個#flushQueue讓 flush 期間再次觸發(fā)的提交排隊串行執(zhí)行避免Promise.all并發(fā)提交導(dǎo)致的驗證錯誤doCommit()UnitOfWork.ts依次派發(fā)beforeFlush、onFlush、afterFlush事件若變更集、集合更新與額外更新均為空則直接返回、不開啟事務(wù)否則根據(jù)implicitTransactions配置決定是否用transactional()包裹persistToDatabase()并支持傳入ctx復(fù)用外層事務(wù)上下文與TransactionEventBroadcastercomputeChangeSets()UnitOfWork.ts遍歷移除棧、IdentityMap 與持久化棧級聯(lián)計算變更集并把刪除后又以相同主鍵重新創(chuàng)建的情況識別為DELETE_EARLY以正確處理實體重建場景。FlushMode枚舉定義了 flush 的觸發(fā)時機(jī)packages/core/src/enums.ts模式行為COMMIT commitEM 延遲 flush直到當(dāng)前事務(wù)提交時。AUTO auto默認(rèn)模式僅在必要時才 flush。ALWAYS always每次查詢前都 flush。關(guān)于 flush 模式與事務(wù)細(xì)節(jié)參見 Unit of Work 與 事務(wù)。加載關(guān)聯(lián)Reference 與 populate關(guān)系不會自動加載。默認(rèn)情況下關(guān)系屬性只包含一個引用——只含主鍵、不含其他數(shù)據(jù)的對象const book await em.findOne(Book, 1); console.log(book.author); // Reference: { id: 5 } console.log(book.author.name); // undefined - not loaded!要加載關(guān)聯(lián)實體使用populate選項const book await em.findOne(Book, 1, { populate: [author] }); console.log(book.author.name); // John Doe - loaded!三種加載策略MikroORM 支持三種加載策略策略說明適用場景select-in按關(guān)系層級逐層發(fā)起獨(dú)立查詢使用IN子句一對多關(guān)系避免笛卡爾積爆炸joined單條帶 JOIN 的查詢多對一關(guān)系或需要基于關(guān)聯(lián)列過濾時balanced默認(rèn)多對一用joined一對多用select-in通用場景兼顧兩者優(yōu)點(diǎn)// 使用特定策略 const books await em.find(Book, {}, { populate: [author, tags], strategy: LoadStrategy.JOINED, });從源碼看EntityLoader在構(gòu)建 populate 計劃時會根據(jù)策略為每個關(guān)聯(lián)屬性選擇實現(xiàn)路徑select-in策略為嵌套關(guān)聯(lián)補(bǔ)充按主鍵集合批量加載的字段EntityLoader.ts并針對自引用關(guān)系在joined與select-in之間做權(quán)衡EntityLoader.ts即使不顯式傳策略populate: [*]這類通配場景也會退回select-in以規(guī)避 JOIN 笛卡爾積EntityLoader.ts。詳細(xì)的策略對比見 Loading Strategiespopulate 的完整用法見 Populating Relations。QueryBuilder原始數(shù)據(jù) vs 水合實體QueryBuilder僅 SQL 驅(qū)動提供兩種獲取結(jié)果的方式qb.execute()—— 原始數(shù)據(jù)直接返回驅(qū)動給出的普通 JavaScript 對象不經(jīng)過 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()—— 水合實體返回完整水合的實體實例并注冊進(jìn) IdentityMapconst users await em.createQueryBuilder(User) .select(*) .where({ active: true }) .getResult(); // users [User { id: 1, name: John }, User { id: 2, name: Jane }] // 這些是被管理的實體變更會被追蹤何時用哪種只讀查詢、不需要變更追蹤時用execute()需要把結(jié)果當(dāng)作實體繼續(xù)操作時用getResult()。從EntityManager.ts源碼看em.createQueryBuilder()在事務(wù)內(nèi)等場景還會自動fork({ keepTransactionContext: true })以保證查詢與當(dāng)前事務(wù)共享上下文EntityManager.ts。QueryBuilder 完整文檔見 QueryBuilder。序列化實體到普通對象的兩種路徑MikroORM 提供兩種將實體轉(zhuǎn)換為普通對象的方式隱式序列化對實體調(diào)用toJSON()或toObject()時序列化由加載時的populate提示驅(qū)動const user await em.findOne(User, 1, { populate: [books], fields: [name, books.title], }); const dto wrap(user).toObject(); // 只包含: id, name, books[].id, books[].title關(guān)鍵行為只有被 populate 的關(guān)系才會序列化為對象未 populate 的關(guān)系序列化為外鍵值fields選項控制輸出中出現(xiàn)哪些屬性。顯式序列化需要完全控制時使用serialize()輔助函數(shù)import { serialize } from mikro-orm/core; const dto serialize(user, { populate: [books, profile], exclude: [password], forceObject: true, });這會忽略原有的 populate 提示讓你精確指定包含與排除的內(nèi)容。序列化器的實現(xiàn)位于 packages/core/src/serialization/EntitySerializer.ts 與 packages/core/src/serialization/EntityTransformer.ts。所有選項包括序列化分組見 Serializing。驅(qū)動架構(gòu)一套核心多數(shù)據(jù)庫適配MikroORM 使用驅(qū)動driver抽象支持多種數(shù)據(jù)庫。mikro-orm/core包承載與數(shù)據(jù)庫無關(guān)的邏輯EntityManager、UnitOfWork、IdentityMap各驅(qū)動包提供數(shù)據(jù)庫特定的實現(xiàn)。SQL 驅(qū)動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 驅(qū)動mikro-orm/mongodb—— 使用原生 MongoDB 驅(qū)動沒有 QueryBuilder改用em.find()配合過濾器對象。功能SQL 驅(qū)動MongoDB 驅(qū)動QueryBuilder完整支持不可用事務(wù)ACID 事務(wù)MongoDB 事務(wù)4.0關(guān)系外鍵、JOIN引用無 JOIN遷移Schema diff不需要無 schemaM:N 關(guān)系中間表擁有方側(cè)引用數(shù)組驅(qū)動抽象的核心是 packages/core/src/drivers/IDatabaseDriver.ts 與 packages/core/src/drivers/DatabaseDriver.ts平臺差異如$ilike、$overlap等 PostgreSQL 專屬操作符見 packages/core/src/enums.ts由各驅(qū)動包內(nèi)的 Platform 實現(xiàn)處理。絕大多數(shù) MikroORM 功能在所有驅(qū)動上行為一致。主要差異在于MongoDB 沒有 JOIN 支持因此不支持按關(guān)聯(lián)實體的屬性過濾——你需要從擁有側(cè)查詢或?qū)?shù)據(jù)做反規(guī)范化。事件系統(tǒng)實體生命周期的鉤子MikroORM 在實體生命周期關(guān)鍵節(jié)點(diǎn)觸發(fā)事件??捎檬录嶓w級onInit、onLoad、beforeCreate、afterCreate、beforeUpdate、afterUpdate、beforeDelete、afterDelete、beforeUpsert、afterUpsertFlush 級beforeFlush、onFlush、afterFlush事務(wù)級beforeTransactionStart、afterTransactionStart、beforeTransactionCommit、afterTransactionCommit、beforeTransactionRollback、afterTransactionRollback事件可通過生命周期鉤子實體方法上的裝飾器或事件訂閱者獨(dú)立類處理Entity() class User { BeforeCreate() setCreatedAt() { this.createdAt new Date(); } } // 或通過訂閱者 class UserSubscriber implements EventSubscriberUser { getSubscribedEntities() { return [User]; } beforeCreate(args: EventArgsUser) { args.entity.createdAt new Date(); } }事件分發(fā)的核心是 packages/core/src/events/EventManager.ts 與 packages/core/src/events/EventSubscriber.ts事務(wù)事件由 packages/core/src/events/TransactionEventBroadcaster.ts 在事務(wù)開始/提交/回滾時廣播。UnitOfWork.dispatchOnLoadEvent()UnitOfWork.ts展示了onLoad事件的觸發(fā)時機(jī)——實體加載后、且存在對應(yīng)監(jiān)聽器時才派發(fā)并用__onLoadFired防止重復(fù)觸發(fā)。完整事件參考見 Events and Hooks。延伸閱讀Entity Manager —— EntityManager API 使用指南Unit of Work —— 變更追蹤與 flush 模式Identity Map —— 請求上下文與 forkPopulating Relations —— 加載關(guān)聯(lián)實體Loading Strategies —— joined 與 select-in 策略對比Serializing —— 實體轉(zhuǎn) DTOEvents and Hooks —— 生命周期事件Transactions —— 事務(wù)管理贊分享后端【免費(fèi)下載鏈接】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點(diǎn)擊查看免費(fèi)下載相關(guān)推薦MikroORM 7 架構(gòu)深度解析Data Mapper、Identity Map 與 Unit of Work 的完整實現(xiàn)鏈路MikroORM 7 架構(gòu)深度解析Data Mapper、Identity Map 與 Unit of Work 的完整實現(xiàn)鏈路 本文基于 MikroORM后端MikroORM 入門指南基于 Data Mapper、Unit of Work 與 Identity Map 的 TypeScript ORMMikroORM 入門指南基于 Data Mapper、Unit of Work 與 Identity Map 的 TypeScript ORM MikroO后端MikroORM 實戰(zhàn)指南基于 Data Mapper、Unit of Work 與 Identity Map 的 TypeScript ORM 快速上手MikroORM 實戰(zhàn)指南基于 Data Mapper、Unit of Work 與 Identity Map 的 TypeScript ORM 快速上手 M后端創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考