☰
在 MikroORM 中定义与刷新 PostgreSQL 物化视图(Materialized Views)实体
2026/9/26 0:28:40 网站建设 项目流程
  • 后端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/mi/mikro-orm
点击查看免费下载

物化视图(Materialized View)将查询结果物理存储在数据库中,以牺牲数据新鲜度为代价换取更快的读取性能。本篇指南基于当前仓库(mikro-orm)的官方文档与源码实现,完整讲解如何在 MikroORM 中以四种方式定义物化视图实体、控制创建时的数据填充(WITH DATA/WITH NO DATA)、通过refreshMaterializedView()手动与并发刷新、像普通实体一样查询,以及 Schema 生成器对物化视图的自动创建、删除与变更检测。读完本文,你将掌握一套可直接落地的物化视图集成方案,并理解其底层 SQL 生成与只读约束的实现原理。

物化视图与普通视图的本质区别

普通视图(View)在查询时才动态执行其定义中的 SQL,数据始终与底层表保持一致,但每次读取都有计算开销。物化视图则把查询结果预先计算并落盘存储,读取时直接扫描已物化的数据,因此读性能显著更好;代价是数据可能过期——底层表发生变化后,物化视图不会自动同步,必须显式执行刷新(REFRESH MATERIALIZED VIEW)才能反映最新数据。

在 MikroORM 中,视图实体(包括普通视图与物化视图)都通过实体选项中的view字段声明,两者仅差一个开关:

  • 普通视图:view: true(或view: {});
  • 物化视图:view: { materialized: true }。

定义物化视图实体

创建物化视图实体只需在实体选项中设置view: { materialized: true },并同时提供expression(物化视图的定义 SQL,即AS之后的查询语句)与tableName。MikroORM 支持四种等价的定义方式,你可以根据自己的元数据提供方式(装饰器反射、ts-morph 静态分析或纯代码定义)任选其一。

方式一:defineEntity + class(推荐,类型安全)

用defineEntity描述 Schema,再导出一个继承该 Schema 的 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);

这里name是实体名称(用于 MikroORM 元数据与关系解析),tableName是物化视图在数据库中的真实名称,expression是视图定义体。p.integer().primary()声明主键——虽然物化视图实体在 ORM 层是只读的,但主键仍可用于标识与查询条件,这也是后续「并发刷新」所需的唯一索引能够被 Schema 生成器感知的基础。

方式二:defineEntity(纯代码定义)

如果不需要实体类实例,直接导出defineEntity的返回值即可:

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装饰器时,把view: { materialized: true }与expression放进实体选项;属性则照常用@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方式在代码写法上与装饰器方式完全一致,区别在于元数据是在编译期由 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; }

仓库中的集成测试也印证了这一配置形式:在 view-entities.postgres.test.ts 中,测试实体AuthorStatsMaterialized使用view: { materialized: true },AuthorStatsNoData使用view: { materialized: true, withData: false },且二者都声明了主键属性。测试还验证了实体元数据会自动带上三个标志(L976-L981):

const meta = orm.getMetadata().get(AuthorStatsMaterialized); expect(meta.view).toBe(true); expect(meta.materialized).toBe(true); expect(meta.readonly).toBe(true);

也就是说,只要声明了view: { materialized: true },ORM 会自动把该实体标记为只读(readonly: true),详见下文「只读行为」一节。

创建时不填充数据:withData: false

默认情况下,物化视图创建时会立即执行定义查询并填充数据,对应 PostgreSQL 的CREATE MATERIALIZED VIEW ... 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 创建时底层表还是空的——此时物化视图定义中的关联查询无数据可物化,先用WITH NO DATA建好结构,待数据就绪后再刷新;二是希望精确控制首次数据填充的时机,把昂贵的聚合计算推迟到业务低峰期统一执行。

从源码看,withData直接决定了生成的 SQL 后缀。PostgreSQL 方言的 SchemaHelper 在 PostgreSqlSchemaHelper.ts 中实现:

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,且 Schema 生成器在调用时使用了view.withData ?? true兜底(SqlSchemaGenerator.ts),因此不显式声明时始终按「带数据创建」处理。

一个值得注意的细节:对于WITH NO DATA的物化视图,Schema 生成器会跳过其索引的创建——因为视图还没有数据可索引,索引会在后续REFRESH填充数据后、由下一次schema:update补建。该行为在 SqlSchemaGenerator.ts 的注释与分支中有明确体现。

刷新物化视图

物化视图缓存了查询结果,因此要让数据反映底层表的最新状态,必须刷新它。MikroORM 在PostgreSqlEntityManager(以及pglite共用的基类BasePostgreSqlEntityManager)上提供了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, {});

并发刷新

PostgreSQL 支持并发刷新物化视图(REFRESH MATERIALIZED VIEW CONCURRENTLY),刷新过程中视图仍可被读取,不会阻塞查询。使用并发刷新的前提是物化视图上至少存在一个唯一索引,否则 PostgreSQL 会直接报错:

// Refresh concurrently (requires unique index on the view) await em.refreshMaterializedView(AuthorStats, { concurrently: true });

底层实现

refreshMaterializedView的完整实现位于 BasePostgreSqlEntityManager.ts,其调用链可以总结为三步:

  1. 校验实体类型:通过this.getMetadata(entityName)获取元数据,若meta.view或meta.materialized不为真,立即抛出Entity ${meta.className} is not a materialized view。仓库测试 view-entities.postgres.test.ts 专门验证了向普通实体Author2调用该方法会抛出此错误;
  2. 确定目标 Schema:schema = meta.schema ?? this.config.get('schema')——优先使用实体上显式声明的schema,否则回退到全局配置的默认 schema;
  3. 生成并执行 SQL:交给当前平台的 SchemaHelper 生成REFRESH MATERIALIZED VIEW语句并通过this.execute(sql)执行。

对应 SQL 模板在 PostgreSqlSchemaHelper.ts:

override refreshMaterializedView(name: string, schema?: string, concurrently = false): string { const concurrent = concurrently ? ' concurrently' : ''; return `refresh materialized view${concurrent} ${this.quote(this.getTableName(name, schema))}`; }

即concurrently: true时生成refresh materialized view concurrently "author_stats_matview",否则生成不带concurrently的普通刷新语句。

测试 view-entities.postgres.test.ts 演示了完整的「写入→刷新→读到新数据」工作流:先插入新作者与书籍并 flush,此时物化视图仍只显示旧数据;调用refreshMaterializedView后重新查询,新作者才出现在结果中——这正是物化视图「手动刷新才更新」语义的直接验证。

查询物化视图

物化视图实体与普通实体无异,可以完全使用 MikroORM 的查询 API:

// 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' });

查询条件支持 MikroORM 的全部操作符体系(如$gte、$in、$like等),物化视图实体也可以出现在关系、QueryBuilder、原生 SQL 等场景中。仓库测试 view-entities.postgres.test.ts 中,em.find(AuthorStatsMaterialized, {})返回的实体数据与物化视图内容完全一致,确认了查询路径与普通实体相同。

只读行为

物化视图实体会被自动标记为只读(readonly: true)。尝试对其实例做修改并 flush,MikroORM不会生成任何 UPDATE 语句——变更既不会写入物化视图(物化视图本就无法直接写入),也不会被静默应用到其他地方:

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

这一自动行为同样被源码与测试双重确认:元数据测试断言了meta.readonly === true(view-entities.postgres.test.ts)。若业务上确实需要「写入」物化视图数据,正确的做法是刷新其依赖的底层表数据后,再调用refreshMaterializedView()让视图重新物化。

Schema 生成

Schema 生成器(orm.schema)对物化视图提供了一等支持,创建、删除、更新三条命令都会自动包含物化视图的处理:

// 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

创建物化视图时,默认生成的 SQL 如下(对应withData默认true):

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时,尾部变为with no data:

create materialized view "author_stats_matview" as select ... with no data;

删除时生成的是带cascade的语句(PostgreSqlSchemaHelper.ts):

drop materialized view if exists "author_stats_matview" cascade

测试 view-entities.postgres.test.ts 验证了getDropSchemaSQL()输出中包含drop materialized view与视图名。

变更检测与依赖排序

Schema 更新(schema:update)不仅会创建与删除物化视图,还会检测物化视图之间的依赖关系。在 SqlSchemaGenerator.ts 中,生成器会对每个视图定义的FROM/JOIN子句做正则匹配,如果视图 A 的定义引用了视图 B,则 B 必须先于 A 创建,从而保证依赖顺序正确。

仓库还提供了专门的 diffing 测试 materialized-view-diffing.postgres.test.ts(含对应快照),用于验证 Schema 生成器对物化视图变更(如定义变化)的检测与增量 SQL 生成。

元数据加载

物化视图也会被 Schema 内省(introspection)识别。PostgreSQL 方言通过查询pg_matviews系统视图加载物化视图及其定义、填充状态(ispopulated),见 PostgreSqlSchemaHelper.ts:loadMaterializedViews会把is_populated映射为withData字段存入DatabaseSchema的DatabaseView对象(DatabaseSchema.ts),同时加载其上的索引——这意味着从已有数据库反向生成实体(entity generator)时也能识别物化视图。

限制

使用物化视图实体时需明确以下边界:

  • 仅支持 PostgreSQL:物化视图只在 PostgreSQL 方言(含pglite)上受支持。其他数据库在使用view: { materialized: true }时会直接报错。这一点在源码中有明确证据——基类 SchemaHelper.ts 中createMaterializedView、dropMaterializedViewIfExists、refreshMaterializedView、getListMaterializedViewsSQL的默认实现全部抛出Not supported by given driver,仅 PostgreSQL 方言覆写了这些方法;
  • 没有自动刷新:MikroORM 不会在每次查询或每次 flush 后自动刷新物化视图,数据的新鲜度完全由你控制。必须手动调用refreshMaterializedView(),或在数据库层面配置刷新机制(如触发器、pg_cron定时任务、应用层调度器等)。

最佳实践

  1. 按数据变化频率选择视图类型:对频繁变化的数据,普通视图(view: true)反而是更合适的选择——它始终实时,无需维护刷新周期;物化视图适合变化不频繁、或能接受一定数据滞后的聚合统计场景(报表、排行榜、计数等)。

  2. 为常用查询列建立索引:物化视图支持索引,应针对高频查询条件建索引。例如为下面的列建立唯一索引与非唯一索引:

    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 生成器感知,你也可以直接让实体主键对应的唯一索引随视图一起创建。

  3. 在低峰期调度刷新:刷新物化视图会重新执行聚合查询并重写整块数据,开销较大。建议使用数据库调度器(如pg_cron)或应用层定时任务,把刷新安排在业务低流量时段;对于WITH NO DATA创建的空视图,首次填充也应选在数据就绪后的低峰期。

  4. 生产环境优先使用并发刷新:如果应用在刷新期间仍需要读取该视图,务必使用{ concurrently: true }(前提是视图上有唯一索引),避免刷新过程中的锁阻塞读请求;刷新频率低、允许短暂不可读的场景,普通刷新开销更小。

  5. 监控视图体积:物化视图会持续占用磁盘空间,且每次刷新都会产生写放大。应监控其大小增长,对超大数据集考虑分区(partitioning)等策略,防止物化视图成为新的存储瓶颈。

  • 后端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/mi/mikro-orm
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询