☰
MikroORM 多 Schema 使用指南:实体定义、运行时切换与 SQLite ATTACH DATABASE 实战
2026/9/26 2:54:51 网站建设 项目流程
  • 后端

【免费下载链接】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
点击查看免费下载

本篇技术指南基于 MikroORM 7.0 官方文档(docs/versioned_docs/version-7.0/multiple-schemas.md)编写,系统讲解如何在 MySQL、PostgreSQL、SQLite/libSQL 中将实体定义到多个 Schema(MySQL 语境中即 database),涵盖实体级 Schema 声明、EntityManager/EntityRepository/QueryBuilder运行时切换、通配符 Schema、默认 Schema 与 SQLiteATTACH DATABASE多库挂载等完整实战方案。读完本文,你将掌握多租户数据隔离、读写分离数据库分库、日志/用户数据独立存储等场景下的 MikroORM 配置与编码方式,并能结合源码与测试理解其底层实现原理。

一、多 Schema 支持概览与使用前提

在 MySQL、PostgreSQL 以及通过ATTACH DATABASE的 SQLite 中,都可以把实体定义到多个 Schema 上。MySQL 术语中称为 database,但从实现角度看它就是 schema。MikroORM 官方文档强调了一个重要前提:

要使用多 Schema,你的连接需要能访问所有这些 Schema(单个 MikroORM 实例中不支持多连接)。

也就是说,多 Schema 方案依赖"一个连接 + 多个命名空间",而不是为每个库建立独立连接池。这一前提与后文 SQLiteattachDatabases的实现完全吻合——从 BaseSqliteConnection.ts 的源码可以看到,所有附加数据库都是通过同一条连接的attach databaseSQL 语句挂载的。

二、在实体上声明 Schema

最简单的方式是直接在实体上通过schema选项声明,或者在自定义表名tableName中带上 Schema 前缀:

@Entity({ schema: 'first_schema' }) export class Foo { ... } // 或者使用带 schema 前缀的自定义表名 @Entity({ tableName: 'second_schema.bar' }) export class Bar { ... }

此后照常使用这些实体即可。生成的 SQL 会把该tableName值作为表名使用,因此只要连接能访问对应 Schema,一切都会按预期工作。例如 PostgreSQL 驱动最终生成的查询形如select ... from "first_schema"."foo",这一点可以在 multiple-schemas.postgres.test.ts 的 SQL 断言中看到(如select "a0".* from "n1"."author" as "a0" ...、from "n5"."book" ...)。

tableName与schema两种方式的效果等价,推荐优先使用schema选项,因为它语义更清晰,且在通配符 Schema、SchemaGenerator等场景下行为更可预期。

三、运行时指定 Schema:EntityManager / EntityRepository / QueryBuilder

除了实体级声明,还可以在查询时动态指定 Schema,适合"多租户一张表、按租户切换库"的场景:

// 通过 FindOptions 指定 schema const user = await em.findOne(User, { ... }, { schema: 'client-123' });

EntityRepository的find/findOne等方法同样接受该选项;QueryBuilder则通过.withSchema()方法在生成 SQL 时限定 Schema。

在测试 attach-database.sqlite.test.ts 中,"should use dynamic schema from FindOptions" 用例验证了em.findOne(UserProfile, { id }, { schema: 'users_db' })可以跨附加数据库正确读取实体。

向指定 Schema 写入数据

要创建实体到特定 Schema,需要使用QueryBuilder的withSchema():

const qb = em.createQueryBuilder(User); await qb.insert({ email: 'foo@bar.com' }).withSchema('client-123');

此外em.insert()/em.insertMany()也支持通过选项指定 Schema,这在 multiple-schemas.postgres.test.ts 的 "use different schema via options in em.insert/Many" 用例中有完整验证:em.insert(Book, book31, { schema: 'n3' })与em.insertMany(Book, [book51, book52], { schema: 'n5' })会分别生成insert into "n3"."book" ...与insert into "n5"."book" ... returning "id"。

需要注意的是:同一个实体集合中的不同实例可以分属不同 Schema 用于持久化,但加载时一次只能从一个 Schema 读取,因为单个查询无法跨多个 Schema 联表(注释见 multiple-schemas.postgres.test.ts)。

四、在 EntityManager 上设置默认 Schema

如果不想在每个实体或每次操作上都声明 Schema,可以.fork()一个 EntityManager 并设置默认 Schema:

const fork = em.fork({ schema: 'client-123' }); await fork.findOne(User, { ... }); // 等价于 const user = await em.findOne(User, { ... }, { schema: 'client-123' });

创建实体时,fork 出的 EM 同样会套用默认 Schema:

const fork = em.fork({ schema: 'client-123' }); const user = new User(); user.email = 'foo@bar.com'; await fork.persist(user).flush(); // 等价于 const qb = em.createQueryBuilder(User); await qb.insert({ email: 'foo@bar.com' }).withSchema('client-123');

从源码看,这一行为由 EntityManager.fork() 实现:fork.#schema = options.schema ?? em.#schema(见 EntityManager.ts),即 fork 出来的新实例会继承父 EM 的 Schema,并允许通过options.schema覆盖。

运行时设置或清除 Schema

em.schema = 'client-123'; // 直接赋值 const fork = em.fork({ schema: 'client-1234' }); fork.schema = null; // 清除默认 schema

EntityManager.schema是上下文感知的:如果在 RequestContext 处理器 内执行,全局 EM 会返回当前请求上下文对应的 Schema。这使得同一份代码在多租户请求中无需手动传参即可命中正确的 Schema。

五、通配符 Schema:一个实体对应多个 Schema

某些场景下,同一实体需要在多个 Schema 中同时存在(例如每个租户一个库)。MikroORM 支持用'*'声明通配符 Schema:

@Entity({ schema: '*' }) export class Book { @PrimaryKey() id!: number; @Property({ nullable: true }) name?: string; @ManyToOne(() => Author, { nullable: true, deleteRule: 'cascade' }) author?: Author; @ManyToOne(() => Book, { nullable: true }) basedOn?: Book; }

这类实体在默认情况下会被SchemaGenerator忽略,因为生成器无法凭空猜测要建在哪个 Schema;你必须通过create/update/drop方法的schema选项或 CLI 的--schema参数明确指定目标 Schema。

在运行时,通配符 Schema 会被按以下优先级替换为实际 Schema:

  1. FindOptions.schema
  2. EntityManager.schema
  3. ORM 配置中的schema选项

这一点在 multiple-schemas.postgres.test.ts 中得到充分验证:测试先对n2~n5分别执行orm.schema.update({ schema: 'nX' })(见该文件第 79-82 行),再通过orm.config.set('schema', 'n2')(第 83 行)让通配符实体默认落到n2。flush 后实体的 Schema 会被保存(wrap(book).getSchema()返回'n2'),身份标识映射键(Identity Map key)也会带上 Schema 前缀,如'Book-n2:1'、'BookTag-n5:4'(见该文件第 119-133 行、第 221-239 行),证明不同 Schema 中的同名实体在 Unit of Work 中互不混淆。

关于迁移的注意事项

目前多 Schema 动态实体不支持通过 ORM 迁移处理:迁移总是忽略通配符 Schema 实体,必须显式使用SchemaGenerator。考虑到这类实体的动态属性,合理的做法是仅在需要时动态同步 Schema,例如放到某个 API 端点中按需执行。如果仍然希望使用 ORM 迁移,则需要手动把动态 Schema 的 SQL 语句追加到迁移文件里,并建议对这些查询使用safe模式({ safe: true })。

六、SQLite 多库:ATTACH DATABASE

SQLite 通过ATTACH DATABASE命令支持多 Schema:可以将额外数据库文件挂载到同一条连接上,每个附加库充当一个独立 Schema,表通过schema.table_name语法访问。

配置 attachDatabases

使用attachDatabases选项指定连接时要挂载的数据库:

import { MikroORM } from '@mikro-orm/sqlite'; // 或 @mikro-orm/libsql const orm = await MikroORM.init({ dbName: './main.db', entities: [Author, Book, UserProfile, LogEntry], attachDatabases: [ { name: 'users_db', path: './users.db' }, { name: 'logs_db', path: '/var/data/logs.db' }, ], });

attachDatabases类型为{ name: string; path: string }[],其语义在 Configuration.ts 中有完整注释:"SQLite/libSQL: databases to attach on connection. Each attached database acts as a schema, accessible viaschema.tablesyntax. Entities can reference attached databases via@Entity({ schema: 'db_name' })."。

相对路径会根据baseDir选项解析(未设置时基于当前工作目录)。这一行为在 BaseSqliteConnection.ts 中实现:fs.absolutePath(db.path, baseDir)之后生成attach database '<path>' as '<name>'语句;attach-database.sqlite.test.ts 的 "should resolve relative paths from baseDir" 用例验证了相对路径确实落在baseDir下。

定义挂载库中的实体

附加数据库中的实体通过schema选项引用数据库名,主库的 schema 可写为'main'(也可省略)。以下四种实体定义方式等价:

defineEntity + class(推荐,类型安全)

import { defineEntity, p } from '@mikro-orm/core'; // 主库实体(主库 schema 可省略) const AuthorSchema = defineEntity({ name: 'Author', schema: 'main', properties: { id: p.number().primary(), name: p.string(), }, }); export class Author extends AuthorSchema.class {} AuthorSchema.setClass(Author); // 附加库实体 const UserProfileSchema = defineEntity({ name: 'UserProfile', schema: 'users_db', properties: { id: p.number().primary(), username: p.string(), }, }); export class UserProfile extends UserProfileSchema.class {} UserProfileSchema.setClass(UserProfile);

defineEntity

import { defineEntity, p } from '@mikro-orm/core'; export const Author = defineEntity({ name: 'Author', schema: 'main', properties: { id: p.number().primary(), name: p.string(), }, }); export const UserProfile = defineEntity({ name: 'UserProfile', schema: 'users_db', properties: { id: p.number().primary(), username: p.string(), }, });

reflect-metadata(装饰器)

@Entity({ schema: 'main' }) class Author { @PrimaryKey() id!: number; @Property() name!: string; } @Entity({ schema: 'users_db' }) class UserProfile { @PrimaryKey() id!: number; @Property() username!: string; }

ts-morph

// 与 reflect-metadata 相同,仅元数据来源不同(ts-morph 通过静态分析生成元数据) @Entity({ schema: 'main' }) class Author { ... } @Entity({ schema: 'users_db' }) class UserProfile { ... }

实体间可以像普通实体一样建立关系:附加库中的实体通过schema指向所属库,挂载后所有库的表都可通过schema.table访问。

Schema Generator 对附加库的完整支持

schema generator对附加数据库提供完整支持,会:

  • 根据实体的schema选项在正确的附加数据库中建表
  • 检测并对所有附加数据库中的表做 diff
  • 为每个数据库生成正确的迁移 SQL
// 在全部数据库(主库 + 附加库)中建表 await orm.schema.create(); // 跨所有数据库更新 schema await orm.schema.update();

对应测试 attach-database.sqlite.test.ts 覆盖了这些行为:pragma database_list能列出main、users_db、logs_db(第 113-122 行);schemaHelper.getAllTables()返回main.main_author、users_db.user_profile、logs_db.log_entry等(第 203-215 行);schema.update()能检测到users_db.user_profile上手工添加的bio列并生成drop column bio(第 253-270 行);getCreateSchemaSQL()不会输出create schema语句(SQLite 使用 ATTACH 而非 CREATE SCHEMA),但仍包含附加库建表语句(第 272-279 行)。

附加库连接的生命周期细节

对于 libSQL 驱动,附加数据库是"按连接保存的状态":一旦底层连接被回收重建,挂载信息会丢失。因此 BaseSqliteConnection.ts 把pragma foreign_keys = on与附加语句一起定义为getConnectionSetupSql()(连接设置 SQL),在连接重建时重放。测试 "attached databases survive a long-lived connection"(attach-database.sqlite.test.ts)通过vi.useFakeTimers模拟时间前进 60 秒后仍能查询附加库实体,验证了这一机制;"replaying the setup restores the state a recycled connection lost"(第 488-501 行)则手动 detach 后调用replayConnectionSetup()恢复挂载状态。

SQLite 附加库的限制

  • libSQL 远程连接:使用远程 libSQL URL(libsql://、https://)时不支持ATTACH DATABASE,只能挂载本地文件数据库。该校验在 LibSqlConnection.ts 中实现,抛错信息为 "ATTACH DATABASE is not supported for remote libSQL connections";attach-database.sqlite.test.ts 对libsql://与https://两种远程 URL 均有断言。
  • 跨库外键:SQLite 允许同一连接内附加库之间的外键,但 SQL 语法中被引用的表名不能包含 Schema 前缀——MikroORM 会自动处理这一差异。
  • 事务:所有附加数据库共享同一条连接内的事务作用域。

七、深入源码:Schema 的底层流转

结合上述特性,可以从源码梳理 Schema 的完整流转链路:

  1. 配置层:attachDatabases定义于 Configuration.ts,仅对 SQLite/libSQL 驱动生效;SqlitePlatform.supportsSchemas()会根据attachDatabases是否存在返回true(见 SqlitePlatform.ts)。
  2. 连接层:SQLite 连接建立时依次执行pragma foreign_keys = on与附加库语句(BaseSqliteConnection.ts);libSQL 驱动在连接前校验远程 URL 并拒绝附加(LibSqlConnection.ts)。
  3. EM 层:fork({ schema })把 Schema 存为 fork 实例的私有字段(EntityManager.ts),查询、插入时作为默认值参与 SQL 拼接。
  4. 执行层:ChangeSetPersister根据withSchema参数决定持久化 SQL 是否携带 Schema 限定(ChangeSetPersister.ts),通配符 Schema 在运行时按FindOptions.schema→EntityManager.schema→ ORM 配置schema的优先级解析。
  5. Identity Map 层:Schema 参与身份标识键的构成(Entity-schema:id),保证同一主键在不同 Schema 中是不同的实体实例,避免 Unit of Work 串扰(见 multiple-schemas.postgres.test.ts)。

八、典型应用场景小结

  • 多租户隔离:每个租户一个 Schema,配合通配符 Schema +em.fork({ schema })或FindOptions.schema,一套实体服务所有租户,按请求上下文动态切换。
  • 分库分表的数据分离:将日志、用户资料、业务主数据拆到不同数据库文件(SQLiteattachDatabases)或不同 Schema(PostgreSQL/MySQL),实体间仍可保持关系与级联操作。
  • 动态 Schema 同步:对通配符实体使用SchemaGenerator的create/update/drop({ schema })或 CLI--schema参数按需建表/改表,迁移文件以safe模式手动补充动态 SQL。

需要提醒的是:多 Schema 依赖单连接访问全部命名空间,若你的数据库部署要求独立连接(例如不同实例、不同账号),则应考虑多个 MikroORM 实例而非本文所述的单实例多 Schema 方案。

参考资料

  • 官方文档:docs/versioned_docs/version-7.0/multiple-schemas.md
  • 配置定义:packages/core/src/utils/Configuration.ts
  • SQLite 附加库实现:packages/sql/src/dialects/sqlite/BaseSqliteConnection.ts
  • libSQL 远程校验:packages/libsql/src/LibSqlConnection.ts
  • 持久化层 Schema 处理:packages/core/src/unit-of-work/ChangeSetPersister.ts
  • 测试验证:tests/features/attach-database/attach-database.sqlite.test.ts、tests/features/multiple-schemas/multiple-schemas.postgres.test.ts
  • 后端

【免费下载链接】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
点击查看免费下载
上一篇:5分钟快速上手:ncmdumpGUI免费解锁网易云音乐NCM文件终极指南
下一篇:CANN ops-math 静默数据损坏检测算子 aclnnSilentCheckV2 接口使用指南

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

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

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

立即咨询