- 后端
【免费下载链接】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 的多 Schema 支持展开,讲解在 MySQL、PostgreSQL 等数据库中如何通过schema选项、tableName前缀、EntityManager默认 Schema 与QueryBuilder.withSchema()在多个 Schema 之间切换查询与写入,并深入剖析 v5 引入的通配符 Schema(schema: '*')在多租户场景下的工作原理及其与SchemaGenerator、Migrations 的协作边界。读完本文,你将掌握从"静态多 Schema 建模"到"运行时动态切换 Schema"的完整实战方案。
一、什么是多 Schema:MySQL 与 PostgreSQL 的差异
在 MySQL 和 PostgreSQL 中,实体可以定义在多个 Schema 中。MySQL 术语里通常称之为"数据库"(database),但从实现角度看它就是 Schema。MikroORM 在 SQL 方言层统一抽象了"Schema"这一概念,因此两套数据库在使用方式上完全一致。
使用多 Schema 的前提条件非常关键:
连接(connection)必须能够访问所有 Schema;单个 MikroORM 实例不支持多个连接。
这意味着你不需要(也不能)为每个 Schema 单独初始化一个 ORM 实例,而是由同一个连接、同一个实体元数据在运行时决定 SQL 落到哪个 Schema。这一约束在源码层面贯穿始终:ConnectionOptions中只有一个可选的默认schema字段(见 Configuration.ts),后续所有 Schema 切换都发生在查询执行层。
二、在实体定义阶段指定 Schema
定义实体时,有两种等价方式告诉 MikroORM 该实体属于哪个 Schema:
@Entity({ schema: 'first_schema' }) export class Foo { ... } // 或者将 schema 名拼进自定义表名 @Entity({ tableName: 'second_schema.bar' }) export class Bar { ... }两种方式最终殊途同归:生成的 SQL 会直接使用带 Schema 前缀的完整表名(如second_schema.bar)。只要连接拥有对应 Schema 的访问权限,查询、插入、更新、删除以及关系加载都会正常工作,业务代码无需感知 Schema 的存在。
三、查询时按操作指定 Schema
除了在实体定义时写死 Schema,你还可以在每次查询时动态指定 Schema,三个入口均支持:
EntityManager:em.findOne(User, {...}, { schema: 'client-123' })EntityRepository:同样在 FindOptions 中传入schemaQueryBuilder:qb.withSchema('client-123')
典型的场景是"同一实体、多个租户/客户分库":实体定义保持一份,运行时按登录用户的上下文传入不同的 Schema 名。FindOptions.schema是运行时优先级最高的 Schema 来源之一(另一来源是EntityManager.schema与 ORM 配置中的schema),后文通配符 Schema 一节会详细说明这三者的替换顺序。
四、写入指定 Schema:QueryBuilder.withSchema()
与查询不同,通过EntityManager的常规persist流程创建实体时,实体落库的 Schema 由实体元数据决定。要在写入时动态指定 Schema,需要显式使用QueryBuilder:
const qb = em.createQueryBuilder(User); await qb.insert({ email: 'foo@bar.com' }).withSchema('client-123');withSchema()的底层实现非常直接:它把 Schema 存入查询构建器的内部状态,后续生成 SQL 时据此限定表名。对应实现见 QueryBuilder.ts:
withSchema(schema?: string): this { this.ensureNotFinalized(); this.#state.schema = schema; return this; }注意ensureNotFinalized()的存在:一旦查询被 finalize(例如已执行或已拼接),就不能再修改 Schema,这与offset、setLockMode等链式方法的保护机制一致。
五、EntityManager 默认 Schema:fork 与上下文感知
如果每个操作都要手动传schema过于繁琐,可以对EntityManager执行.fork()并指定默认 Schema,此后该 fork 上的所有操作都会自动落到该 Schema:
const fork = em.fork({ schema: 'client-123' }); await fork.findOne(User, { ... }); // 等价于 const user = await em.findOne(User, { ... }, { schema: 'client-123' });创建实体时同样生效——fork 会将默认 Schema 应用到新实体的写入:
const fork = em.fork({ schema: 'client-123' }); const user = new User(); user.email = 'foo@bar.com'; await fork.persistAndFlush(user); // 等价于 const qb = em.createQueryBuilder(User); await qb.insert({ email: 'foo@bar.com' }).withSchema('client-123');默认 Schema 也可以在 fork 之后随时设置或清除:
em.schema = 'client-123'; const fork = em.fork({ schema: 'client-1234' }); fork.schema = null; // 清除默认 Schema,回退到实体/配置定义这里有一个值得注意的实现细节:EntityManager.schema的 getter/setter 是**上下文感知(context-aware)**的。源码见 EntityManager.ts:
get schema(): string | undefined { return this.getContext(false).#schema; } set schema(schema: string | null | undefined) { this.getContext(false).#schema = schema ?? undefined; }getContext(false)意味着:当代码运行在 RequestContext 处理器 内部时,全局的em.schema读写会透明地落到当前请求上下文的 fork 上,从而实现"每个请求一个 Schema、全局代码零侵入"的多租户隔离模式。
六、通配符 Schema:一个实体对应多套 Schema
自 v5 起,MikroORM 支持定义"可存在于多个 Schema"的实体——只要把schema写成通配符'*':
@Entity({ schema: '*' }) export class Book { @PrimaryKey() id!: number; @Property({ nullable: true }) name?: string; @ManyToOne(() => Author, { nullable: true, onDelete: 'cascade' }) author?: Author; @ManyToOne(() => Book, { nullable: true }) basedOn?: Book; }通配符 Schema 实体是典型的多租户建模方案:每个租户拥有结构完全相同的独立 Schema(如client-1、client-2、client-3),而实体定义只有一份。运行时,通配符会被替换为以下三处之一(按优先级):
FindOptions.schema(单次查询指定);EntityManager.schema(fork 或em.schema设置);- ORM 配置
ConnectionOptions.schema(默认兜底)。
替换逻辑发生在 SQL 生成阶段,因此查询、加载关系、级联删除(如示例中的onDelete: 'cascade')都会自动落到目标 Schema。
6.1 SchemaGenerator 与 CLI --schema
通配符 Schema 实体默认会被SchemaGenerator忽略,因为生成器不知道要为哪个 Schema 建表。需要显式指定目标 Schema:
- 编程方式:
createSchema/updateSchema/dropSchema方法传入schema选项; - CLI 方式:
npx mikro-orm schema:create --schema client-123等命令的--schema参数。
这一行为在源码中有明确印证:SqlSchemaGenerator会收集所有schema === '*'的元数据并将其表名放入排除列表,只有传入includeWildcardSchema时才跳过该剪枝逻辑。见 SqlSchemaGenerator.ts:
const wildcardSchemaTables = options.includeWildcardSchema ? [] : [...this.metadata.getAll().values()].filter(meta => meta.schema === '*').map(meta => meta.tableName); fromSchema.prune(options.schema, wildcardSchemaTables); toSchema.prune(options.schema, wildcardSchemaTables);也就是说,不带includeWildcardSchema时,通配符实体被从 diff 的 from/to 两端剪除,避免对未指定 Schema 的实体生成误操作。
6.2 关于 Migrations 的注意事项
当前版本中,通配符 Schema 实体无法直接通过 Migrations 管理:Migrations 会始终忽略通配符 Schema 实体,必须显式使用SchemaGenerator。
考虑到这类实体的动态特性(新租户随时可能被创建),合理的实践是:
- 在 API 端点中动态同步 Schema(例如注册新租户时调用
SchemaGenerator为该租户建表); - 仍然保留 ORM Migrations 管理普通实体,但将动态 Schema 的建表 SQL手动写入迁移文件;
- 对这类动态查询建议开启
safe模式(safe: true),避免破坏性操作在误判场景下造成数据损失。
七、整体工作流:从静态定义到动态切换
综合以上内容,一套完整的多 Schema 工作流可以概括为:
- 静态多 Schema:实体用
schema或tableName前缀固定归属;连接需有全部 Schema 权限; - 运行时查询切换:
FindOptions.schema(单次)或em.fork({ schema })/em.schema(会话级); - 运行时写入切换:
QueryBuilder.withSchema()显式插入目标 Schema; - 多租户动态 Schema:实体声明
schema: '*',结合RequestContext按请求解析实际 Schema,建表通过SchemaGenerator+--schema参数按需同步,迁移文件手动补充动态 SQL。
其中第 4 步与请求上下文配合时,EntityManager.schema的上下文感知 getter/setter 是关键枢纽:全局代码无需改动,每个请求通过 RequestContext 自动获得各自的 Schema 隔离。
八、源码路径速查
| 能力点 | 源码位置 |
|---|---|
QueryBuilder.withSchema()状态写入 | QueryBuilder.ts |
EntityManager.schema上下文感知读写 | EntityManager.ts |
| 连接默认 Schema 配置项 | Configuration.ts |
| SchemaGenerator 通配符实体剪枝 | SqlSchemaGenerator.ts |
| 关联文档 | multiple-schemas.md、identity-map.md、migrations.md |
如需深入了解 Schema 与数据库连接的配置细节,可继续阅读 configuration.md 与 schema-generator.md 两篇指南。
- 后端
【免费下载链接】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 多 Schema 使用完全指南:多数据库架构、通配符 Schema 与 SQLite ATTACH DATABASE 实战
MikroORM 多 Schema 使用完全指南:多数据库架构、通配符 Schema 与 SQLite ATTACH DATABASE 实战 在 MySQL、P
后端io-ts与JSON Schema:如何将运行时类型转换为JSON Schema定义
io ts与JSON Schema:如何将运行时类型转换为JSON Schema定义 想要在TypeScript项目中实现运行时类型安全,同时生成标准的JSON
后端PostgREST Schema 暴露与多租户切换完全指南:db-schemas、Profile 头与动态 Schema
PostgREST Schema 暴露与多租户切换完全指南:db schemas、Profile 头与动态 Schema 导读 本文基于 PostgREST 官
后端API网关
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考