- 后端
【免费下载链接】mikro-orm
TypeScript 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.
物化视图(Materialized View)将查询结果物理存储下来,以数据新鲜度为代价换取更快的读取性能;与普通视图不同,物化视图必须显式刷新才能反映底层数据的变化。本文基于 MikroORM 当前仓库(v7.0 文档线)讲解如何通过实体定义、refreshMaterializedView()调用与 Schema Generator 全流程管理 PostgreSQL 物化视图,读者读完可以掌握四种实体定义方式、WITH DATA/WITH NO DATA的创建控制、并发刷新、只读语义以及物化视图索引的 diff 与生成细节。
物化视图与普通视图的本质区别
在 MikroORM 中,视图(View)是一种特殊的实体形态:它不对应真实数据表,而是对应一段SELECT查询表达式。普通视图(view: true)只是查询的"别名",每次查询都会实时执行底层 SQL;物化视图(view: { materialized: true })则把查询结果物理落盘,后续读取直接扫描存储的数据,因此读取更快,但数据是"快照",需要手动刷新。
这一区分在仓库元数据层有明确体现。在 packages/core/src/metadata/types.ts 中,view选项的类型定义为:
view?: boolean | { materialized?: boolean; withData?: boolean };view: true表示普通视图,view: { materialized: true }表示物化视图(仅 PostgreSQL),view: { materialized: true, withData: false }表示创建时不填充数据的物化视图。平台支持度由 packages/core/src/platforms/Platform.ts 的supportsMaterializedViews()控制,基类默认返回false,而 PostgreSQL 平台在 packages/sql/src/dialects/postgresql/BasePostgreSqlPlatform.ts 中将其重写为true——这正是"物化视图仅限 PostgreSQL"限制的底层来源。
定义物化视图实体
创建物化视图实体只需在实体选项中配置view: { materialized: true },并通过expression提供视图的查询定义。MikroORM 支持四种定义风格,下面以一个统计每位作者书籍数量的AuthorStats实体为例逐一说明。
defineEntity + class(推荐)
import { defineEntity, p } from '@mikro-orm/postgresql'; const AuthorStatsSchema = defineEntity({ name: 'AuthorStats', tableName: 'author_stats_matview', view: { materialized: true }, expression: ` select a.id, a.name, count(b.id)::int as book_count from author a left join book b on b.author_id = a.id group by a.id `, properties: { id: p.integer().primary(), name: p.string(), bookCount: p.integer(), }, }); export class AuthorStats extends AuthorStatsSchema.class {} AuthorStatsSchema.setClass(AuthorStats);defineEntity(仅 Schema 常量)
如果不希望定义实体类,只导出 Schema 常量即可:
import { defineEntity, p } from '@mikro-orm/postgresql'; export const AuthorStats = defineEntity({ name: 'AuthorStats', tableName: 'author_stats_matview', view: { materialized: true }, expression: ` select a.id, a.name, count(b.id)::int as book_count from author a left join book b on b.author_id = a.id group by a.id `, properties: { id: p.integer().primary(), name: p.string(), bookCount: p.integer(), }, });reflect-metadata(装饰器 + 元数据反射)
使用@Entity装饰器,并在类属性上使用@PrimaryKey/@Property:
import { Entity, Property, PrimaryKey } from '@mikro-orm/postgresql'; @Entity({ tableName: 'author_stats_matview', view: { materialized: true }, expression: ` select a.id, a.name, count(b.id)::int as book_count from author a left join book b on b.author_id = a.id group by a.id `, }) export class AuthorStats { @PrimaryKey() id!: number; @Property() name!: string; @Property() bookCount!: number; }ts-morph(静态代码分析)
使用 ts-morph 提供元数据时,实体定义与 reflect-metadata 风格完全一致,只是元数据发现机制不同:
import { Entity, Property, PrimaryKey } from '@mikro-orm/postgresql'; @Entity({ tableName: 'author_stats_matview', view: { materialized: true }, expression: ` select a.id, a.name, count(b.id)::int as book_count from author a left join book b on b.author_id = a.id group by a.id `, }) export class AuthorStats { @PrimaryKey() id!: number; @Property() name!: string; @Property() bookCount!: number; }无论哪种方式,实体属性都必须与expression中SELECT返回的列一一对应,属性名遵循 MikroORM 的列名到属性名的映射规则(如book_count映射为bookCount)。
从测试看元数据形态
仓库测试 tests/features/view-entities/view-entities.postgres.test.ts 同样用defineEntity定义了AuthorStatsMaterialized(view: { materialized: true })与AuthorStatsNoData(view: { materialized: true, withData: false }),并在 第 976-981 行 断言其元数据:
expect(meta.view).toBe(true); expect(meta.materialized).toBe(true); expect(meta.readonly).toBe(true);即物化视图实体在元数据层面会自动带有view: true、materialized: true、readonly: true三个标记。
创建无数据的物化视图(WITH NO DATA)
默认情况下,物化视图创建时会立即填充数据(对应 PostgreSQL 的WITH DATA)。如果希望在创建时生成一个空视图、稍后再填充数据,可以设置withData: false:
const AuthorStats = defineEntity({ name: 'AuthorStats', tableName: 'author_stats_matview', view: { materialized: true, withData: false }, // Creates with "WITH NO DATA" expression: `select ...`, properties: { ... }, });这一选项在以下场景特别有用:
- 创建 Schema 时底层表还是空的,立即执行
SELECT没有意义; - 希望完全掌控初始数据填充的时机(先建库建视图,再由应用或定时任务触发首次
REFRESH)。
在底层 SQL 生成层面,该开关由 packages/sql/src/dialects/postgresql/PostgreSqlSchemaHelper.ts 的createMaterializedView方法处理:
override createMaterializedView( name: string, schema: string | undefined, definition: string, withData = true, ): string { const viewName = this.quote(this.getTableName(name, schema)); const dataClause = withData ? ' with data' : ' with no data'; return `create materialized view ${viewName} as ${definition}${dataClause}`; }withData参数默认值为true,只有显式传false时才会追加with no data子句。
刷新物化视图
物化视图缓存了查询结果,数据变更后必须刷新才能看到最新数据。MikroORM 在PostgreSqlEntityManager上提供了refreshMaterializedView方法:
import { MikroORM, EntityManager } from '@mikro-orm/postgresql'; const orm = await MikroORM.init({ ... }); const em = orm.em; // Refresh the materialized view await em.refreshMaterializedView(AuthorStats); // Now queries will return the updated data const stats = await em.find(AuthorStats, {});底层实现解析
该方法定义在 packages/sql/src/dialects/postgresql/BasePostgreSqlEntityManager.ts(pg与pglite的 EntityManager 共同基类)。其核心逻辑为:
- 通过
this.getMetadata(entityName)获取实体元数据; - 校验
meta.view && meta.materialized,若目标实体不是物化视图,则抛出Entity ${meta.className} is not a materialized view错误——测试 第 1036-1038 行 专门验证了这一行为; - 从驱动平台取得 SchemaHelper,拼接 SQL:
refresh materialized view <table>; - 通过
this.execute(sql)执行。
对应的 SQL 生成在 PostgreSqlSchemaHelper.ts 第 218-221 行:
override refreshMaterializedView(name: string, schema?: string, concurrently = false): string { const concurrent = concurrently ? ' concurrently' : ''; return `refresh materialized view${concurrent} ${this.quote(this.getTableName(name, schema))}`; }并发刷新(Concurrent Refresh)
PostgreSQL 支持并发刷新物化视图——刷新过程中读操作不会被阻塞。并发刷新要求物化视图上至少存在一个唯一索引,否则 PostgreSQL 会直接报错:
// Refresh concurrently (requires unique index on the view) await em.refreshMaterializedView(AuthorStats, { concurrently: true });注意:并发刷新要求物化视图上至少有一个唯一索引。缺少唯一索引时 PostgreSQL 会抛出错误。
对应生成的 SQL 为refresh materialized view concurrently "author_stats_matview"。刷新是重量级操作,生产环境中应结合唯一索引与低峰期窗口使用(详见下文"最佳实践")。
刷新后的数据可见性
注意 MikroORM 的 Identity Map 机制:同一 EntityManager 中刷新视图后,已加载的实体快照不会自动失效,建议在刷新后em.clear()或使用 fork 出来的新 EntityManager 查询。测试 第 1014-1034 行 展示了完整链路:插入新作者数据 → 刷新前查询仍只有旧数据 →refreshMaterializedView()→em.clear()→ 再次查询即能看到新数据。
查询物化视图
物化视图实体与普通实体一样,可以使用em.find/em.findOne配合条件进行查询:
// Find all const allStats = await em.find(AuthorStats, {}); // Find with conditions const prolificAuthors = await em.find(AuthorStats, { bookCount: { $gte: 5 }, }); // Find one const authorStats = await em.findOne(AuthorStats, { name: 'Jon Snow' });查询操作直接命中物理存储的数据,因此性能稳定。测试 第 994-1012 行 验证了"插入数据 → 刷新视图 → 查询得到聚合结果"的完整流程。
只读行为
物化视图实体会被自动标记为只读(readonly: true),尝试对其持久化修改会失败:
const stats = await em.findOne(AuthorStats, { id: 1 }); stats.bookCount = 100; // This change won't be persisted await em.flush(); // No UPDATE will be generated for this entity这一行为由元数据层的readonly标记驱动(见前文测试断言),Unit of Work 在flush时不会为只读实体生成任何UPDATE语句。物化视图数据的唯一合法更新途径是执行REFRESH MATERIALIZED VIEW。
Schema 生成
Schema Generator 对物化视图提供全自动支持:
// Create schema (includes CREATE MATERIALIZED VIEW statements) await orm.schema.create(); // Drop schema (includes DROP MATERIALIZED VIEW statements) await orm.schema.drop(); // Update schema (detects changes to materialized views) await orm.schema.update();生成的 SQL
创建物化视图(默认WITH DATA)会生成:
create materialized view "author_stats_matview" as select a.id, a.name, count(b.id)::int as book_count from author a left join book b on b.author_id = a.id group by a.id with data;设置withData: false时:
create materialized view "author_stats_matview" as select ... with no data;源码层面的生成与 diff 逻辑
视图的创建、删除、更新逻辑集中在 packages/sql/src/schema/SqlSchemaGenerator.ts:
- 创建:
appendViewCreation(第 795-812 行)根据view.materialized分发到createMaterializedView,并追加WITH NO DATA视图的索引创建——但跳过withData === false的视图的索引(此时视图还没有数据可索引,索引会在数据填充后的下一次schema:update中创建); - 删除:
drop与update流程(第 248-253 行、第 400-423 行)按依赖顺序先删视图再删表,物化视图走dropMaterializedViewIfExists,生成drop materialized view if exists "xxx" cascade; - 变更检测:packages/sql/src/schema/SchemaComparator.ts 对物化视图的索引使用现有的表 diff 基础设施进行增删改检测,通过构造临时
DatabaseTable复用diffTable逻辑,从而让schema:update能自动发现物化视图上索引的变化。
测试 tests/features/schema-generator/materialized-view-diffing.postgres.test.ts 覆盖了新增/删除/修改物化视图、withData切换、schema:create与schema:update生成索引(GH #7417)等场景,可当作行为规范参考。其中 第 236-268 行 验证了关键细节:withData: false期间不生成索引 diff,切换为withData: true后schema:update才会补建唯一索引,且不会自动刷新视图——索引建立在空数据上是合法的(PostgreSQL 允许对未刷新视图建索引,索引保持空直到下次REFRESH)。
另外要注意,物化视图只支持索引,不支持其他表级结构;其DatabaseView模型(packages/sql/src/typings.ts)仅包含materialized、withData与indexes三个物化视图专属字段。
限制
- 仅限 PostgreSQL:物化视图只在 PostgreSQL 平台受支持。其他数据库平台一旦使用
view: { materialized: true }就会报错。这一限制的根因在 packages/sql/src/schema/SchemaHelper.ts:基类的createMaterializedView、dropMaterializedViewIfExists、refreshMaterializedView、getListMaterializedViewsSQL、loadMaterializedViews全部直接throw new Error('Not supported by given driver'),仅 PostgreSQL 的 SchemaHelper 进行了 override。 - 无自动刷新:MikroORM 不会自动刷新物化视图。必须手动调用
refreshMaterializedView(),或在数据库层面建立刷新机制(触发器、pg_cron 定时任务等)。
最佳实践
选择合适的刷新策略:对于频繁变化的数据,优先考虑普通视图(
view: true);物化视图适合变化不频繁、或可以接受一定时间延迟的数据(聚合报表、统计看板、跨表 JOIN 的昂贵查询)。为物化视图加索引:物化视图支持索引。为高频查询的列建立索引,尤其要满足并发刷新的前提:
CREATE UNIQUE INDEX author_stats_id_idx ON author_stats_matview (id); CREATE INDEX author_stats_book_count_idx ON author_stats_matview (book_count);索引可以在实体元数据中声明,由
schema:create/schema:update自动生成,也可以直接用 SQL 手工创建。调度刷新:使用数据库调度器(pg_cron)或应用层调度,在低流量时段刷新物化视图,避免高峰期的资源争抢。
生产环境使用并发刷新:如果应用在刷新期间仍需读取视图数据,始终使用
{ concurrently: true }(前提是视图上有唯一索引),保证刷新过程中读不阻塞。监控视图体积:物化视图会持续占用磁盘空间,且每次刷新都是全量重建(
REFRESH会锁表或通过并发刷新渐进重建)。需监控其大小,数据量极大时考虑分区策略或其他降级方案。
总结
MikroORM 将 PostgreSQL 物化视图建模为一种特殊的只读实体:通过view: { materialized: true }声明,用expression定义查询,由 Schema Generator 负责CREATE/DROP/UPDATE(含索引 diff),用refreshMaterializedView()手动刷新(可并发),底层 SQL 由 PostgreSQL 专属 SchemaHelper 生成、平台能力由supportsMaterializedViews()统一管控。结合 tests/features/view-entities/view-entities.postgres.test.ts 与 tests/features/schema-generator/materialized-view-diffing.postgres.test.ts 两组测试,可以完整验证"定义 → 建库 → 填充数据 → 刷新 → 查询"的闭环。合理使用物化视图,能在保持 MikroORM 统一实体模型的同时,为读多写少、容忍延迟的聚合查询带来稳定的性能收益。
- 后端
【免费下载链接】mikro-orm
TypeScript 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.
相关推荐
在 MikroORM 中定义与刷新 PostgreSQL 物化视图(Materialized Views)实体
在 MikroORM 中定义与刷新 PostgreSQL 物化视图(Materialized Views)实体 物化视图(Materialized View)将
后端跨平台文本编辑器 Notepad-- 5 步跑通目录搜索与文件对比
跨平台文本编辑器 Notepad 5 步跑通目录搜索与文件对比 你手头有一整目录散落的代码和配置,改一处要翻遍几十上百个文件,Notepad 帮你在一个窗口里搜
桌面应用Materialized View:pybind11物化视图性能优化实战
Materialized View:pybind11物化视图性能优化实战 引言:当Python遇见C++的性能瓶颈 在现代科学计算和机器学习应用中,Python
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考