體(Virtual Entities)實(shí)戰(zhàn):用動(dòng)態(tài) SQL 與 MongoDB 聚合映射只讀實(shí)體)
后端【免費(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.項(xiàng)目地址https://gitcode.com/gh_mirrors/mi/mikro-orm點(diǎn)擊查看免費(fèi)下載本文基于 mikro-orm 官方文檔virtual-entities章節(jié)以 version-6.6 文檔為基準(zhǔn)結(jié)合倉(cāng)庫(kù)中的驅(qū)動(dòng)源碼與測(cè)試用例完整講解虛擬實(shí)體的定義方式、字符串 SQL 表達(dá)式與回調(diào)表達(dá)式的區(qū)別、MongoDB 聚合用法以及查詢時(shí) ORM 底層的執(zhí)行機(jī)制。讀完之后你可以掌握如何把任意 SQL 查詢或聚合管道的結(jié)果直接映射為可查詢的實(shí)體并理解find/count/stream/qb在虛擬實(shí)體上的真實(shí)執(zhí)行路徑。一、什么是虛擬實(shí)體虛擬實(shí)體Virtual Entities不對(duì)應(yīng)任何數(shù)據(jù)庫(kù)表。相反它們?cè)诓樵儠r(shí)動(dòng)態(tài)解析為一條 SQL 查詢?cè)?MongoDB 中是一條聚合管道從而允許把任意形式的查詢結(jié)果映射到實(shí)體上。這類實(shí)體面向只讀場(chǎng)景它們沒(méi)有主鍵因此無(wú)法被 Unit of Work 追蹤變更不能persist/remove/更新。在某種意義上它們類似于數(shù)據(jù)庫(kù)視圖——你可以用它來(lái)代理已經(jīng)存在的原生視圖。兩條關(guān)鍵約束來(lái)自文檔的醒目提示虛擬實(shí)體可以包含標(biāo)量屬性以及單值關(guān)系M:1 和 1:1 的所有者側(cè)。這類關(guān)系始終通過(guò)select-in策略填充表達(dá)式中的列名必須遵循當(dāng)前命名策略。文檔示例中authorName屬性對(duì)應(yīng)的列名就是author_name。與后續(xù)版本的關(guān)系6.6 文檔中寫(xiě)明數(shù)據(jù)庫(kù)視圖目前不受支持虛擬實(shí)體是當(dāng)時(shí)代理原生視圖的替代方案當(dāng)前倉(cāng)庫(kù)中新增了獨(dú)立的 View Entities 章節(jié)見(jiàn) view-entitiesview 實(shí)體由 Schema Generator 生成CREATE VIEW語(yǔ)句并納入遷移追蹤而虛擬實(shí)體在查詢時(shí)才評(píng)估表達(dá)式——兩者定位不同可按需選用。從源碼結(jié)構(gòu)看一個(gè)實(shí)體是否被標(biāo)記為虛擬由元數(shù)據(jù)推導(dǎo)在 EntityMetadata 中存在this.virtual !!this.expression !this.view的判斷即定義了expression且不是 view 實(shí)體就自動(dòng)視為虛擬實(shí)體。二、用字符串 SQL 定義虛擬實(shí)體最直接的寫(xiě)法給實(shí)體元數(shù)據(jù)提供expression其值可以是一個(gè) SQL 字符串。以四種實(shí)體定義風(fēng)格分別給出與原文檔一致的./entities/BookWithAuthor.ts示例2.1 reflect-metadata / ts-morph 裝飾器風(fēng)格Entity({ expression: select b.title, a.name as author_name, ( select group_concat(distinct t.name) from book b join tags_ordered bt on bt.book_id b.id join book_tag t on t.id bt.book_tag_id where b.author_id a.id group by b.author_id ) as tags from author a group by a.id }) export class BookWithAuthor { Property() title!: string; Property() authorName!: string; Property() tags!: string[]; }注意authorName屬性通過(guò) SQL 別名author_name映射遵循默認(rèn)的 snake_case 命名策略tags列由關(guān)聯(lián)子查詢聚合而成。2.2 defineEntity 風(fēng)格import { type InferEntity, defineEntity } from mikro-orm/core; export const BookWithAuthor defineEntity({ name: BookWithAuthor, expression: select b.title, a.name as author_name, ( select group_concat(distinct t.name) from book b join tags_ordered bt on bt.book_id b.id join book_tag t on t.id bt.book_tag_id where b.author_id a.id group by b.author_id ) as tags from author a group by a.id , properties: p ({ title: p.string(), authorName: p.string(), tags: p.type(string[]).$typestring[](), }), }); export interface IBookWithAuthor extends InferEntitytypeof BookWithAuthor {}2.3 EntitySchema 風(fēng)格export interface IBookWithAuthor { title: string; authorName: string; tags: string[]; } export const BookWithAuthor new EntitySchemaIBookWithAuthor({ name: BookWithAuthor, expression: select b.title, a.name as author_name, ( select group_concat(distinct t.name) from book b join tags_ordered bt on bt.book_id b.id join book_tag t on t.id bt.book_tag_id where b.author_id a.id group by b.author_id ) as tags from author a group by a.id , properties: { title: { type: string }, authorName: { type: string }, tags: { type: string[] }, }, });文檔中 reflect-metadata 與 ts-morph 兩個(gè)標(biāo)簽頁(yè)的示例代碼完全相同僅編譯鏈/元數(shù)據(jù)解析方式不同上面 2.1 已統(tǒng)一給出。三、用回調(diào)定義虛擬實(shí)體Query Builder 寫(xiě)法第二種方式是提供一個(gè)回調(diào)函數(shù)返回 Query Builder或字符串、raw片段。回調(diào)接收(em, where, options)三個(gè)參數(shù)Entity({ expression: (em: EntityManager) { return em.createQueryBuilder(Book, b) .select([b.title, a.name as author_name, group_concat(t.name) as tags]) .join(b.author, a) .join(b.tags, t) .groupBy(b.id); }, }) export class BookWithAuthor { Property() title!: string; Property() authorName!: string; Property() tags!: string[]; }defineEntity風(fēng)格的等價(jià)寫(xiě)法export const BookWithAuthor defineEntity({ name: BookWithAuthor, expression: (em: EntityManager) { return em.createQueryBuilder(Book, b) .select([b.title, a.name as author_name, group_concat(t.name) as tags]) .join(b.author, a) .join(b.tags, t) .groupBy(b.id); }, properties: p ({ title: p.string(), authorName: p.string(), tags: p.type(string[]).$typestring[](), }), });EntitySchema風(fēng)格export const BookWithAuthor new EntitySchemaIBookWithAuthor({ name: BookWithAuthor, expression: (em: EntityManager) { return em.createQueryBuilder(Book, b) .select([b.title, a.name as author_name, group_concat(t.name) as tags]) .join(b.author, a) .join(b.tags, t) .groupBy(b.id); }, properties: { title: { type: string }, authorName: { type: string }, tags: { type: string[] }, }, });expression 的類型簽名在 EntityMetadata 類型定義 中expression的完整簽名是expression?: | string | (( em: any, where: ObjectQueryEntity, options: FindOptionsEntity, any, any, any, stream?: boolean, ) MaybePromiseRaw | object | string);從源碼結(jié)構(gòu)看回調(diào)的返回值可以是字符串、Raw片段、Query Builder 實(shí)例或 POJO 數(shù)組且回調(diào)本身可以是async返回MaybePromise。第四個(gè)參數(shù)stream?: boolean用于區(qū)分當(dāng)前是流式查詢還是普通查詢回調(diào)可以據(jù)此選擇返回?cái)?shù)據(jù)庫(kù)游標(biāo)或普通數(shù)組。四、MongoDB用聚合管道定義虛擬實(shí)體在 MongoDB 上expression回調(diào)通常返回一條聚合管道。文檔給出的示例是前面 SQL 示例的粗略等價(jià)物Entity({ expression: (em: EntityManager, where, options) { const $sort { ...options.orderBy } as Dictionary; $sort._id 1; const pipeline: Dictionary[] [ { $project: { _id: 0, title: 1, author: 1 } }, { $sort }, { $match: where ?? {} }, { $lookup: { from: author, localField: author, foreignField: _id, as: author, pipeline: [{ $project: { name: 1 } }] } }, { $unwind: $author }, { $set: { authorName: $author.name } }, { $unset: [author] }, ]; if (options.offset ! null) { pipeline.push({ $skip: options.offset }); } if (options.limit ! null) { pipeline.push({ $limit: options.limit }); } return em.aggregate(Book, pipeline); }, }) export class BookWithAuthor { Property() title!: string; Property() authorName!: string; }要點(diǎn)文檔明確提示由于聚合管道的特性這種寫(xiě)法并不十分人體工學(xué)ergonomicwhere查詢條件以及orderBy、limit、offset等選項(xiàng)必須在你的管道中顯式處理——示例中通過(guò)$match: where ?? {}、$sort、$skip/$limit完成回調(diào)最終返回em.aggregate(Book, pipeline)由驅(qū)動(dòng)等待其結(jié)果并映射為實(shí)體數(shù)據(jù)。驅(qū)動(dòng)側(cè)的實(shí)現(xiàn)印證了這一點(diǎn)MongoDriver.findVirtual 中若meta.expression是函數(shù)則創(chuàng)建一個(gè)新的EntityManager并調(diào)用meta.expression(em, where, options)直接把回調(diào)的返回值當(dāng)作EntityData[]使用streamVirtual 則額外傳入true作為第四個(gè)參數(shù)表示流式場(chǎng)景。五、SQL 驅(qū)動(dòng)底層執(zhí)行機(jī)制理解虛擬實(shí)體在 SQL 數(shù)據(jù)庫(kù)上的行為需要看 AbstractSqlDriver 的實(shí)現(xiàn)。這里區(qū)分兩種expression形態(tài)5.1 字符串表達(dá)式外層子查詢包裝當(dāng)expression是字符串時(shí)findFromVirtual 會(huì)調(diào)用wrapVirtualExpressionInSubquery把原始 SQL 包進(jìn)一個(gè)派生表(${expression}) as qb_alias由一個(gè)普通 QueryBuilder 負(fù)責(zé)在外層應(yīng)用where、orderBy、limit、offset。關(guān)鍵源碼const asKeyword this.platform.usesAsKeyword() ? as : ; native.from(raw((${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}));這帶來(lái)幾個(gè)可驗(yàn)證的行為過(guò)濾與排序是外層的em.find(BookWithAuthor, { authorName: ... }, { orderBy, limit, offset })實(shí)際執(zhí)行的是select ... from (你的完整SQL) as qb where qb.author_name ? order by ... limit ...因此你只能按表達(dá)式結(jié)果集中可見(jiàn)的列做過(guò)濾/排序count 的實(shí)現(xiàn)countVirtual 走同樣的路徑但會(huì)clear(select).clear(limit).clear(offset).count()即對(duì)子查詢結(jié)果直接countstream 支持wrapVirtualExpressionInSubqueryStream 用同一套子查詢包裝邏輯但走連接的stream接口逐行產(chǎn)出實(shí)體適合大結(jié)果集若處于行級(jí)安全RLS會(huì)話上下文中且非顯式事務(wù)上下文驅(qū)動(dòng)會(huì)為其套一層短事務(wù)來(lái)執(zhí)行查詢?cè)创a注釋說(shuō)明了這一意圖。5.2 回調(diào)表達(dá)式fork EM 多形態(tài)返回值當(dāng)expression是函數(shù)時(shí)見(jiàn) findFromVirtual 后半段驅(qū)動(dòng)會(huì)fork調(diào)用方 EM若 options 中帶em使回調(diào)繼承其過(guò)濾器、過(guò)濾參數(shù)與會(huì)話上下文并通過(guò)em.setTransactionContext(options.ctx)保持事務(wù)一致性回調(diào)的返回值按形態(tài)分派返回字符串→ 走與字符串表達(dá)式相同的子查詢包裝返回QueryBuilder→ 取其getFormattedQuery()再包裝返回Raw 片段→ 先用platform.formatQuery格式化再包裝返回POJO 數(shù)組如 MongoDB 場(chǎng)景→ 直接作為結(jié)果返回不再包裝。六、測(cè)試用例中的行為驗(yàn)證倉(cāng)庫(kù)中有一組針對(duì)虛擬實(shí)體的專項(xiàng)測(cè)試可作為行為依據(jù)SQLitevirtual-entities.sqlite.test.tsPostgreSQLvirtual-entities.postgres.test.tsMongoDBvirtual-entities.mongo.test.ts回調(diào)中 filters 的傳遞virtual-entity-callback-filters.sqlite.test.ts相關(guān)快照snapshots含 postgres 與 sqlite 兩份快照以 SQLite 測(cè)試為例它覆蓋了一個(gè)AuthorProfile實(shí)體字符串 SQL、() raw(sql)、() sql、Query Builder 回調(diào)四種表達(dá)形式并驗(yàn)證了以下能力findAndCount 結(jié)果緩存orm.em.findAndCount(AuthorProfile, {}, { cache: 5000 })返回[profiles, 3]且?guī)Ь彺娴牡诙握{(diào)用不再產(chǎn)生新的 SQLmock 日志調(diào)用次數(shù)保持為 4——說(shuō)明虛擬實(shí)體同樣支持cache選項(xiàng)streamorm.em.stream(AuthorProfile, { orderBy: { name: 1, usedTags: 1 } })以異步迭代方式產(chǎn)出實(shí)體且結(jié)果與find一致QueryBuilder 分頁(yè)orm.em.qb(AuthorProfile).limit(2).offset(1).orderBy({ name: asc }).getResult()精確得到[Jon Snow 2, Jon Snow 3]印證了外層子查詢 外層 limit/offset的執(zhí)行模型where 操作符{ $and: [{ name: { $like: Jon% } }, { age: { $gte: 0 } }] }這類復(fù)合過(guò)濾直接作用于子查詢結(jié)果列內(nèi)嵌屬性Embeddable映射表達(dá)式結(jié)果中的列被映射到identity: { foo, bar }對(duì)象屬性序列化時(shí)按實(shí)體屬性輸出。另外GH4628.test.ts 針對(duì)社區(qū)反饋的具體問(wèn)題做了回歸測(cè)試說(shuō)明虛擬實(shí)體的邊界情況如與 QueryBuilder 交互在持續(xù)被覆蓋。七、使用建議與限制小結(jié)定位虛擬實(shí)體只適合讀模型報(bào)表、聚合視圖、跨表聯(lián)查投影不能寫(xiě)入不需要也無(wú)法定義主鍵命名策略SQL 別名必須與命名策略后的列名一致authorName?author_name否則會(huì)映射失敗where/orderBy 的作用面字符串表達(dá)式場(chǎng)景下過(guò)濾、排序、分頁(yè)都作用于表達(dá)式結(jié)果集的外層因此表達(dá)式必須輸出你想要過(guò)濾/排序的列關(guān)系填充實(shí)體上的 M:1 / 1:1 關(guān)系一律走select-in策略MongoDB 差異管道需要自行處理$match/$sort/$skip/$limit且聚合寫(xiě)法靈活性不如 SQL 直觀需要真正的數(shù)據(jù)庫(kù)視圖建表 DDL 納入遷移管理時(shí)參考 View Entities虛擬實(shí)體則適合查詢時(shí)求值的輕量映射例如代理已有的原生視圖或復(fù)雜的聚合查詢。以上行為均以 version-6.6 文檔為基準(zhǔn)源碼引用來(lái)自當(dāng)前倉(cāng)庫(kù)核心類型定義、mikro-orm/sql抽象 SQL 驅(qū)動(dòng)、mikro-orm/mongodb驅(qū)動(dòng)與特性測(cè)試在 6.6 及以上版本中該機(jī)制的核心執(zhí)行路徑保持一致。贊分享后端【免費(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.項(xiàng)目地址https://gitcode.com/gh_mirrors/mi/mikro-orm點(diǎn)擊查看免費(fèi)下載相關(guān)推薦深入解析MegaBasterd跨平臺(tái)MEGA客戶端的技術(shù)架構(gòu)與實(shí)現(xiàn)原理深入解析MegaBasterd跨平臺(tái)MEGA客戶端的技術(shù)架構(gòu)與實(shí)現(xiàn)原理 MegaBasterd作為一款功能豐富的跨平臺(tái)MEGA下載器、上傳器和流媒體套件其技桌面應(yīng)用Doctrine ORM Native SQL 完全指南用 NativeQuery 與 ResultSetMapping 執(zhí)行原生 SQL 并映射為實(shí)體Doctrine ORM Native SQL 完全指南用 NativeQuery 與 ResultSetMapping 執(zhí)行原生 SQL 并映射為實(shí)體 Na數(shù)據(jù)庫(kù)ORM后端Prism-Samples-Wpf導(dǎo)航系統(tǒng)詳解從基礎(chǔ)導(dǎo)航到高級(jí)路由控制Prism Samples Wpf導(dǎo)航系統(tǒng)詳解從基礎(chǔ)導(dǎo)航到高級(jí)路由控制 Prism Samples Wpf導(dǎo)航系統(tǒng)是構(gòu)建現(xiàn)代化WPF應(yīng)用程序的核心組件它提供后端上一篇探索Nano IDRust中的輕量級(jí)唯一ID生成器下一篇SwiftUI布局系統(tǒng)深度解析從FlowLayout到自定義Layout協(xié)議的完整指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考