mikro-orm 虚拟实体(Virtual Entities)实战:用动态 SQL 与 MongoDB 聚合映射只读实体
后端【免费下载链接】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点击查看免费下载本文基于 mikro-orm 官方文档virtual-entities章节以 version-6.6 文档为基准结合仓库中的驱动源码与测试用例完整讲解虚拟实体的定义方式、字符串 SQL 表达式与回调表达式的区别、MongoDB 聚合用法以及查询时 ORM 底层的执行机制。读完之后你可以掌握如何把任意 SQL 查询或聚合管道的结果直接映射为可查询的实体并理解find/count/stream/qb在虚拟实体上的真实执行路径。一、什么是虚拟实体虚拟实体Virtual Entities不对应任何数据库表。相反它们在查询时动态解析为一条 SQL 查询在 MongoDB 中是一条聚合管道从而允许把任意形式的查询结果映射到实体上。这类实体面向只读场景它们没有主键因此无法被 Unit of Work 追踪变更不能persist/remove/更新。在某种意义上它们类似于数据库视图——你可以用它来代理已经存在的原生视图。两条关键约束来自文档的醒目提示虚拟实体可以包含标量属性以及单值关系M:1 和 1:1 的所有者侧。这类关系始终通过select-in策略填充表达式中的列名必须遵循当前命名策略。文档示例中authorName属性对应的列名就是author_name。与后续版本的关系6.6 文档中写明数据库视图目前不受支持虚拟实体是当时代理原生视图的替代方案当前仓库中新增了独立的 View Entities 章节见 view-entitiesview 实体由 Schema Generator 生成CREATE VIEW语句并纳入迁移追踪而虚拟实体在查询时才评估表达式——两者定位不同可按需选用。从源码结构看一个实体是否被标记为虚拟由元数据推导在 EntityMetadata 中存在this.virtual !!this.expression !this.view的判断即定义了expression且不是 view 实体就自动视为虚拟实体。二、用字符串 SQL 定义虚拟实体最直接的写法给实体元数据提供expression其值可以是一个 SQL 字符串。以四种实体定义风格分别给出与原文档一致的./entities/BookWithAuthor.ts示例2.1 reflect-metadata / ts-morph 装饰器风格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属性通过 SQL 别名author_name映射遵循默认的 snake_case 命名策略tags列由关联子查询聚合而成。2.2 defineEntity 风格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 风格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 两个标签页的示例代码完全相同仅编译链/元数据解析方式不同上面 2.1 已统一给出。三、用回调定义虚拟实体Query Builder 写法第二种方式是提供一个回调函数返回 Query Builder或字符串、raw片段。回调接收(em, where, options)三个参数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风格的等价写法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风格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);从源码结构看回调的返回值可以是字符串、Raw片段、Query Builder 实例或 POJO 数组且回调本身可以是async返回MaybePromise。第四个参数stream?: boolean用于区分当前是流式查询还是普通查询回调可以据此选择返回数据库游标或普通数组。四、MongoDB用聚合管道定义虚拟实体在 MongoDB 上expression回调通常返回一条聚合管道。文档给出的示例是前面 SQL 示例的粗略等价物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; }要点文档明确提示由于聚合管道的特性这种写法并不十分人体工学ergonomicwhere查询条件以及orderBy、limit、offset等选项必须在你的管道中显式处理——示例中通过$match: where ?? {}、$sort、$skip/$limit完成回调最终返回em.aggregate(Book, pipeline)由驱动等待其结果并映射为实体数据。驱动侧的实现印证了这一点MongoDriver.findVirtual 中若meta.expression是函数则创建一个新的EntityManager并调用meta.expression(em, where, options)直接把回调的返回值当作EntityData[]使用streamVirtual 则额外传入true作为第四个参数表示流式场景。五、SQL 驱动底层执行机制理解虚拟实体在 SQL 数据库上的行为需要看 AbstractSqlDriver 的实现。这里区分两种expression形态5.1 字符串表达式外层子查询包装当expression是字符串时findFromVirtual 会调用wrapVirtualExpressionInSubquery把原始 SQL 包进一个派生表(${expression}) as qb_alias由一个普通 QueryBuilder 负责在外层应用where、orderBy、limit、offset。关键源码const asKeyword this.platform.usesAsKeyword() ? as : ; native.from(raw((${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}));这带来几个可验证的行为过滤与排序是外层的em.find(BookWithAuthor, { authorName: ... }, { orderBy, limit, offset })实际执行的是select ... from (你的完整SQL) as qb where qb.author_name ? order by ... limit ...因此你只能按表达式结果集中可见的列做过滤/排序count 的实现countVirtual 走同样的路径但会clear(select).clear(limit).clear(offset).count()即对子查询结果直接countstream 支持wrapVirtualExpressionInSubqueryStream 用同一套子查询包装逻辑但走连接的stream接口逐行产出实体适合大结果集若处于行级安全RLS会话上下文中且非显式事务上下文驱动会为其套一层短事务来执行查询源码注释说明了这一意图。5.2 回调表达式fork EM 多形态返回值当expression是函数时见 findFromVirtual 后半段驱动会fork调用方 EM若 options 中带em使回调继承其过滤器、过滤参数与会话上下文并通过em.setTransactionContext(options.ctx)保持事务一致性回调的返回值按形态分派返回字符串→ 走与字符串表达式相同的子查询包装返回QueryBuilder→ 取其getFormattedQuery()再包装返回Raw 片段→ 先用platform.formatQuery格式化再包装返回POJO 数组如 MongoDB 场景→ 直接作为结果返回不再包装。六、测试用例中的行为验证仓库中有一组针对虚拟实体的专项测试可作为行为依据SQLitevirtual-entities.sqlite.test.tsPostgreSQLvirtual-entities.postgres.test.tsMongoDBvirtual-entities.mongo.test.ts回调中 filters 的传递virtual-entity-callback-filters.sqlite.test.ts相关快照snapshots含 postgres 与 sqlite 两份快照以 SQLite 测试为例它覆盖了一个AuthorProfile实体字符串 SQL、() raw(sql)、() sql、Query Builder 回调四种表达形式并验证了以下能力findAndCount 结果缓存orm.em.findAndCount(AuthorProfile, {}, { cache: 5000 })返回[profiles, 3]且带缓存的第二次调用不再产生新的 SQLmock 日志调用次数保持为 4——说明虚拟实体同样支持cache选项streamorm.em.stream(AuthorProfile, { orderBy: { name: 1, usedTags: 1 } })以异步迭代方式产出实体且结果与find一致QueryBuilder 分页orm.em.qb(AuthorProfile).limit(2).offset(1).orderBy({ name: asc }).getResult()精确得到[Jon Snow 2, Jon Snow 3]印证了外层子查询 外层 limit/offset的执行模型where 操作符{ $and: [{ name: { $like: Jon% } }, { age: { $gte: 0 } }] }这类复合过滤直接作用于子查询结果列内嵌属性Embeddable映射表达式结果中的列被映射到identity: { foo, bar }对象属性序列化时按实体属性输出。另外GH4628.test.ts 针对社区反馈的具体问题做了回归测试说明虚拟实体的边界情况如与 QueryBuilder 交互在持续被覆盖。七、使用建议与限制小结定位虚拟实体只适合读模型报表、聚合视图、跨表联查投影不能写入不需要也无法定义主键命名策略SQL 别名必须与命名策略后的列名一致authorName↔author_name否则会映射失败where/orderBy 的作用面字符串表达式场景下过滤、排序、分页都作用于表达式结果集的外层因此表达式必须输出你想要过滤/排序的列关系填充实体上的 M:1 / 1:1 关系一律走select-in策略MongoDB 差异管道需要自行处理$match/$sort/$skip/$limit且聚合写法灵活性不如 SQL 直观需要真正的数据库视图建表 DDL 纳入迁移管理时参考 View Entities虚拟实体则适合查询时求值的轻量映射例如代理已有的原生视图或复杂的聚合查询。以上行为均以 version-6.6 文档为基准源码引用来自当前仓库核心类型定义、mikro-orm/sql抽象 SQL 驱动、mikro-orm/mongodb驱动与特性测试在 6.6 及以上版本中该机制的核心执行路径保持一致。赞分享后端【免费下载链接】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点击查看免费下载相关推荐深入解析MegaBasterd跨平台MEGA客户端的技术架构与实现原理深入解析MegaBasterd跨平台MEGA客户端的技术架构与实现原理 MegaBasterd作为一款功能丰富的跨平台MEGA下载器、上传器和流媒体套件其技桌面应用Doctrine ORM Native SQL 完全指南用 NativeQuery 与 ResultSetMapping 执行原生 SQL 并映射为实体Doctrine ORM Native SQL 完全指南用 NativeQuery 与 ResultSetMapping 执行原生 SQL 并映射为实体 Na数据库ORM后端Prism-Samples-Wpf导航系统详解从基础导航到高级路由控制Prism Samples Wpf导航系统详解从基础导航到高级路由控制 Prism Samples Wpf导航系统是构建现代化WPF应用程序的核心组件它提供后端上一篇探索Nano IDRust中的轻量级唯一ID生成器下一篇SwiftUI布局系统深度解析从FlowLayout到自定义Layout协议的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Atlas 300V 24G推理加速卡实测:YOLOv5部署全链路与调优

Atlas 300V 24G推理加速卡实测:YOLOv5部署全链路与调优

接到手这块Atlas 300V 24G的时候,我第一反应也是去查它到底算运算加速卡还是别的什么卡。等真正把YOLOv5跑起来,又折腾了一阵驱动和模型转换之后,才发现网上一堆帖子说得云里雾里。这篇文章就围绕两个高频问题来写:Atlas 300V 24G…

2026/9/25 5:13:02 阅读更多 →
Cursor新模型Composer来了,TaoToken统一Key怎么接?

Cursor新模型Composer来了,TaoToken统一Key怎么接?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 5:13:02 阅读更多 →
在 TEN Framework 中构建异步 HTTP 服务器 Python 扩展:aio_http_server_python 源码解析与实战

在 TEN Framework 中构建异步 HTTP 服务器 Python 扩展:aio_http_server_python 源码解析与实战

人工智能AI Agent多模态语音AI 应用 【免费下载链接】ten-framework Open-source framework for conversational voice AI agents 项目地址: https://gitcode.com/TEN-framework/ten-framework 点击查看 免费下载 导读 aio_http_server_python 是 TEN Framework 官…

2026/9/25 5:12:01 阅读更多 →

最新新闻

Atlas 300V部署YOLO全流程:昇腾推理卡环境搭建与模型转换实战

Atlas 300V部署YOLO全流程:昇腾推理卡环境搭建与模型转换实战

“Atlas部署YOLO”这六个字,是我最近在好几个AI相关的社群里见得最多的一句话。点进去一看,问的人大多一脸迷茫,手里刚好有一张Atlas 300V Pro(24G)推理卡,或者是公司刚采购了一批昇腾设备,领导…

2026/9/25 11:07:42 阅读更多 →
约定式提交(Conventional Commits)1.0.0-beta.1 规范详解:以结构化提交信息驱动版本管理与 CHANGELOG 自动化

约定式提交(Conventional Commits)1.0.0-beta.1 规范详解:以结构化提交信息驱动版本管理与 CHANGELOG 自动化

文档 【免费下载链接】conventionalcommits.org The conventional commits specification 项目地址: https://gitcode.com/gh_mirrors/co/conventionalcommits.org 点击查看 免费下载 本篇文章以仓库中 content/v1.0.0-beta.1/index.zh-hans.md 为规范原文主体&…

2026/9/25 11:07:42 阅读更多 →
nvim-tree.lua 贡献开发指南:质量检查、帮助文档生成与 Pull Request 规范全解

nvim-tree.lua 贡献开发指南:质量检查、帮助文档生成与 Pull Request 规范全解

开发工具 【免费下载链接】nvim-tree.lua A file explorer tree for neovim written in lua 项目地址: https://gitcode.com/gh_mirrors/nv/nvim-tree.lua 点击查看 免费下载 导读:本文以 nvim-tree.lua 仓库的 CONTRIBUTING.md 为主体,系统…

2026/9/25 11:07:42 阅读更多 →
MiniMax-H3-Comfy-NPU 性能调优指南:BF16 对 INT8、8/21/50 步 768P 实测数据对比(含单卡低显存方案)

MiniMax-H3-Comfy-NPU 性能调优指南:BF16 对 INT8、8/21/50 步 768P 实测数据对比(含单卡低显存方案)

MiniMax-H3-Comfy-NPU 性能调优指南:BF16 对 INT8、8/21/50 步 768P 实测数据对比(含单卡低显存方案) 【免费下载链接】MiniMax-H3-Comfy-NPU 项目地址: https://ai.gitcode.com/Ascend-SACT/MiniMax-H3-Comfy-NPU MiniMax-H3-Comfy-…

2026/9/25 11:07:42 阅读更多 →
Chrome DevTools MCP 配 TaoToken:让 AI 无缝接管浏览器调试会话的配置骨架

Chrome DevTools MCP 配 TaoToken:让 AI 无缝接管浏览器调试会话的配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 11:07:41 阅读更多 →
CSP-S初赛复习不是刷题,而是知识结构体检

CSP-S初赛复习不是刷题,而是知识结构体检

1. 初赛不是“刷题大赛”,而是“知识结构体检表”CSP-S 一轮(初赛)复习知识点总——这七个字背后,藏着太多学生踩过的坑。我带过三届CSP-S提高组集训班,每年9月一开学,总有学生拿着《信息学奥赛一本通》从头…

2026/9/25 11:06:41 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →