- 后端
【免费下载链接】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 7 允许开发者不必在配置中逐个罗列实体类,而是通过 glob 模式让 ORM 自动扫描、动态导入并识别符合命名约定的实体文件。本指南以 docs/versioned_docs/version-7.2/folder-based-discovery.md 为核心骨架,结合核心包源码与 CLI 实现,系统讲解entities/entitiesTs配置、文件命名约定、glob 模式进阶用法、mikro-orm debug与mikro-orm discovery:export两个 CLI 命令,以及显式引用与文件夹发现的取舍。读完本文,你将能在大中型项目中正确配置自动发现、排查路径解析问题,并借助 barrel 文件同时获得文件夹发现的便利性与显式引用的类型安全。
为什么需要文件夹发现:从显式列表到 glob 模式
默认情况下,MikroORM 的entities配置项接收一组实体类引用:
export default defineConfig({ entities: [User, Article, Tag, BaseEntity], });这种显式写法直观、类型安全,但每当新增实体时都必须手动更新配置,实体数量一多,配置会变得冗长且容易遗漏。文件夹发现(Folder-based Discovery)正是为解决这一问题而设计:用 glob 模式描述"实体文件长什么样",让 ORM 在启动时根据文件命名约定自动找出所有实体。
其配置入口非常简单(见 Configuration.ts 中对entities、entitiesTs两个配置项的源码注释):
import { defineConfig } from '@mikro-orm/sqlite'; export default defineConfig({ // Glob patterns for compiled JavaScript files entities: ['dist/**/*.entity.js'], // Glob patterns for TypeScript source files (used in development) entitiesTs: ['src/**/*.entity.ts'], // ... });其中:
entities:指向编译后的 JavaScript 实体文件,用于生产运行;entitiesTs:指向TypeScript 源码实体文件,用于开发期(通过tsx、swc等工具直接运行 TS 的场合)。
工作原理:preferTs判定与动态导入
从源码看,整个文件夹发现的流程可以拆成三步:
- 判定当前运行环境:
MikroORM.init()会计算preferTs值——orm.config.get('preferTs', Utils.detectTypeScriptSupport())(见 MikroORM.ts)。detectTypeScriptSupport()(见 Utils.ts)会检查多种信号:ts-node、环境变量MIKRO_ORM_CLI_ALWAYS_ALLOW_TS、TS_JEST、VITEST、Bun、命令行参数中的.ts文件,以及tsx、@swc-node/register、@oxc-node/core等加载器; - 选择目标模式集合:
findEntities(preferTs)中执行const targets = preferTs && entitiesTs.length > 0 ? entitiesTs : entities;(见 MetadataDiscovery.ts)——即运行在 TS 环境时优先用entitiesTs,否则回退到entities; - 动态导入并扫描:当
targets中存在字符串路径时,调用@mikro-orm/core/file-discovery模块的discoverEntities(paths, { baseDir })执行 glob 展开、逐文件动态导入,并收集其中的实体类与EntitySchema。
discoverEntities的实现在 discover-entities.ts,几个值得注意的细节:
- 跳过
.d.ts声明文件,只处理.ts/.js/.mts/.mjs/.cts/.cjs等实际实现文件; - 一个文件里既导出
EntitySchema又导出其关联类实现时,只保留 schema,避免重复注册(见getEntityClassOrSchema,discover-entities.ts); - 对装饰器实体通过
MetadataStorage.isKnownEntity(item.name)判断是否为已知实体,对defineEntity/EntitySchema实体则通过EntitySchema.REGISTRY查找已注册 schema; - 所有文件以
fs.dynamicImport方式异步导入,这与下方"同步初始化限制"直接相关。
重要约定:
entities必须指向编译后的 JS 文件,entitiesTs必须指向 TS 源文件,两者不可混用。若在运行编译产物时误把entities指向 TS 源码,或反之,都会导致实体加载失败或类型信息丢失。
文件命名约定:.entity.ts后缀
文件夹发现依赖"命名约定",最常见的约定是.entity.ts后缀。典型项目结构如下:
src/ ├── modules/ │ ├── user/ │ │ └── user.entity.ts │ ├── article/ │ │ ├── article.entity.ts │ │ └── tag.entity.ts │ └── common/ │ └── base.entity.ts └── mikro-orm.config.ts配套配置为:
export default defineConfig({ entities: ['dist/**/*.entity.js'], entitiesTs: ['src/**/*.entity.ts'], });只要新实体文件按xxx.entity.ts命名并放入src目录,就会自动被src/**/*.entity.ts匹配,无需再改配置。官方测试 tests/MikroORM.test.ts 验证了基于complex-entities/**/*.entity.ts的 glob 能同时发现普通装饰器实体与defineEntity类实体。
Glob 模式进阶:多模式、负模式与 brace expansion 限制
路径解析使用 Node.js 原生 glob 实现,因此支持标准 glob 语法:
const orm = await MikroORM.init({ // 递归匹配 dist 目录下所有 .entity.js 文件 entities: ['./dist/**/*.entity.js'], // 多个模式(数组元素可叠加) entities: ['./dist/modules/**/*.entity.js', './dist/shared/**/*.entity.js'], // 负模式(negation)排除特定文件 entities: ['./dist/**/*.entity.js', '!./dist/**/*.test.entity.js'], });要点说明:
**递归匹配任意层级目录,*匹配单层路径片段;- 负模式以
!开头,可用于排除测试实体、临时文件等; - 模式既可以写相对路径(相对
baseDir,默认process.cwd(),见 Configuration.ts),也可以写绝对路径。
:::note brace expansion 限制 Node.js 原生 glob不支持brace expansion 语法,例如src/{entities,modules}/*.ts这类模式无法直接用于entities/entitiesTs。如果确实需要,可以借助tinyglobby在配置加载阶段先展开成具体路径列表:
import { glob } from 'tinyglobby'; export default defineConfig({ entities: await glob(['src/{entities,modules}/*.ts']), });注意defineConfig接收的配置对象在顶层可以使用顶层 await(在 ESM 配置文件中),展开后的路径数组会作为普通字符串数组传入。 :::
调试发现:mikro-orm debug
当文件夹发现表现不符合预期(实体缺失、路径错误等)时,第一个排查手段是 CLI 的debug命令:
npx mikro-orm debug从 DebugCommand.ts 的实现看,它会输出:
- 当前加载的 CLI 配置:搜索过的配置文件路径、搜索的配置名(
contextName)、TypeScript 支持状态与加载器; - driver 依赖及其版本:如
- postgresql 0.0.0之类的驱动包版本列表; - 数据库连接结果:成功或失败;
preferTs判定:若显式设置了preferTs,会打印提示"will useentitiesTsarray"或"will useentitiesarray",并提醒编译产物运行时应设为false;- 实体路径解析结果:分别统计
entities与entitiesTs数组中的引用数(实体类)与路径数(glob/文件夹),并逐个检查路径是否存在,标注(found)或(not found)。
官方测试 tests/features/cli/DebugCommand.test.ts 展示了完整输出格式,可用于对照你的实际输出。例如当配置entities: ['./dist/entities-1', './dist/entities-2']时,输出中会明确显示每个路径是否存在——这能帮你快速定位"glob 写对了但目录名/层级不对"这类问题。
显式引用 vs 文件夹发现:如何选择
原文档给出了一张完整的对比表,这里原样保留并补充实践建议:
| Aspect | Explicit (entities: [User]) | Folder-based (entities: ['**/*.entity.js']) |
|---|---|---|
| Setup complexity | More code | Less code |
| Refactoring | IDE-supported | Manual pattern updates |
| Build tools | Works everywhere | May need configuration |
| Performance | Faster startup | Slightly slower (file scanning) |
| Error detection | Compile-time | Runtime |
何时使用显式引用
- 中小型项目,实体数量有限;
- 使用 webpack、esbuild 等打包器(打包器无法处理运行时动态导入的 glob 路径);
- 需要最大化 IDE 支持与类型安全(重命名实体时 IDE 可以同步更新引用);
- 使用
defineEntity(官方推荐的方式)时,显式引用是最稳妥的搭配。
何时使用文件夹发现
- 大型项目,实体数量多且持续增长;
- 使用 ts-morph 元数据提供器(
TsMorphMetadataProvider)的装饰器实体,需要扫描源码做类型推断; - 实体分散在多个模块目录中;
- 希望新增实体时完全不用改配置。
从源码角度补充一点:文件夹发现在启动期需要 glob 扫描 + 逐文件动态导入(discoverEntities,见 discover-entities.ts),因此启动会"略慢";而显式引用直接处理已加载的类引用,且编译期即可发现实体不存在/未导出等错误,这正是表中"启动速度"与"错误检测时机"两行的依据。
同步初始化限制:new MikroORM()不支持文件夹发现
文件夹发现依赖异步动态导入,因此只有异步的MikroORM.init()支持文件夹发现;同步构造函数new MikroORM()无法使用 glob 路径。这一点在源码中有两处印证:
- MetadataDiscovery.ts:
discoverReferences遇到字符串路径直接抛出'Folder based discovery requires the asyncMikroORM.init()method.'; - MikroORM.ts:同步构造函数的文档注释明确列出限制——"folder-based discovery not supported、ORM extensions are not autoloaded"。
// 正确:异步初始化支持文件夹发现 const orm = await MikroORM.init({ entities: ['dist/**/*.entity.js'], }); // 错误:同步构造必须使用显式实体引用 const orm = new MikroORM({ entities: ['dist/**/*.entity.js'], // ✗ 不支持 // entities: [User, Article], // ✓ 只能这样写 });多个实体位置:混合引用与 glob 模式
entities与entitiesTs数组允许同时包含实体类引用和字符串路径,用于需要优先加载基类实体或混合发现策略的场景:
import { BaseEntity } from './entities/base.entity.js'; export default defineConfig({ entities: [ BaseEntity, // 显式引用:基类实体 'dist/modules/**/*.entity.js', // glob 模式:其余实体 ], entitiesTs: [ BaseEntity, 'src/modules/**/*.entity.ts', ], });这种做法在以下场景特别有用:
- 基类实体需要先于子类加载:如
BaseEntity作为抽象基类,通过显式引用保证其元数据先行注册; - 部分实体使用特殊命名(无法被既有 glob 覆盖),需要显式补充;
- 逐步迁移:从全显式引用迁移到文件夹发现的过程中,可以两者并存。
源码层面,findEntities会先把数组中的字符串路径收集进paths,类引用收集进processed,然后对路径执行discoverEntities后合并处理(见 MetadataDiscovery.ts),因此两种形式的顺序与混用都是安全的。
生成 barrel 文件:discovery:export
文件夹发现虽然省事,但失去了显式引用的类型安全与打包器兼容性。discovery:export命令提供了一种两全其美的中间方案:扫描实体源码,生成一个带显式导入的 TypeScript barrel 文件,之后配置改为引用该文件中的entities数组。
npx mikro-orm discovery:export命令会从配置的entitiesTs(优先)或entities数组中提取字符串路径(也可用--path显式指定),扫描并动态导入实体文件,生成类似下面的文件:
// This file was generated by MikroORM CLI. Do not edit manually. // Re-run `mikro-orm discovery:export` to update. import { Article } from './entities/Article.js'; import { User } from './entities/User.js'; export const entities = [ Article, User, ] as const;然后在配置中使用生成的文件:
import { entities } from './entities.generated'; export default defineConfig({ entities });这样你得到的是:
- 文件夹发现的便利性:新增实体后重跑一次命令即可,无需手写 import;
- 显式引用的好处:对打包器友好(无运行时动态导入)、启动更快(直接使用类引用)、编译期检查(实体未导出会报错)。
命令选项
| Flag | Type | Description |
|---|---|---|
-p, --path | string[] | 实体源文件的 glob 模式(可多个) |
-o, --out | string | 输出文件路径(默认生成在 ORM 配置文件同目录下的entities.generated.ts) |
-d, --dump | boolean | 打印到 stdout 而不写文件 |
实现细节(见 DiscoveryExportCommand.ts):
- 路径解析顺序:
--path参数 > 配置entitiesTs中的字符串路径 > 配置entities中的字符串路径,否则报错No entity paths found in config. Use --path to specify entity source locations.(DiscoveryExportCommand.ts); - 实体识别逻辑与运行时发现一致:跳过
__esModule、跳过与EntitySchema关联的类实现、按MetadataStorage.isKnownEntity判定装饰器实体(DiscoveryExportCommand.ts); - 生成的输出文件头部带"由 CLI 生成、请勿手改"的注释,重新执行命令即可更新;
- 生成的
entities数组带有as const,可推导出Database实体元组类型; - 切换配置后重跑需显式传
--path:一旦配置改为entities(引用数组,不再含字符串路径),命令无法再从配置提取路径,此时应显式指定:
npx mikro-orm discovery:export --path './src/entities/*.ts'与 Kysely 集成的类型增强
discovery:export生成的 barrel 文件不仅是实体数组,还会额外导出两个与 Kysely 类型集成相关的内容(详见 kysely.md 的 Generating Entity Exports with the CLI 一节):
Database类型:即typeof entities,可用于MikroORM<Driver, EM, Database>等接受实体元组的泛型位置;EntityManager类型与值:一个绑定到驱动包、且携带实体元组信息(通过幻影属性'~entities'嫁接)的实体感知EntityManager别名。它同时以type和const形式导出,既可作为 NestJS 等 DI 容器的注入令牌,也可作为构造参数的类型标注,确保em.getKysely(opts)能保持完整的表名、列名类型推断。
如果你自定义了EntityManager子类,可参考 kysely.md 的做法:写一个紧邻的包装文件,把生成的Database元组嫁接到自己的 EM 子类上,再让服务从包装文件导入,这样重跑discovery:export不会覆盖你的定制代码。
常见问题与最佳实践小结
entities与entitiesTs别混用:编译产物跑entities,TS 源码跑entitiesTs,这是文件夹发现能正确工作的前提;- Vitest/ESM 下的
ERR_UNKNOWN_FILE_EXTENSION:在 ESM 项目中使用文件夹发现并配合 Vitest 等测试框架时,可能遇到TypeError: Unknown file extension ".ts"。原因是 MikroORM 内部执行动态导入,而 Vitest 无法自动转换这些导入。解决方式是覆盖dynamicImportProvider(详见 guide/01-first-entity.md 的 ESM 提示):
export default defineConfig({ // ... dynamicImportProvider: id => import(id), });- 排查顺序:先
npx mikro-orm debug确认加载了哪个配置文件、preferTs判定结果、每个实体路径是否存在,再检查 glob 模式与命名约定是否一致; - 元数据缓存注意:使用文件夹发现时,元数据缓存与运行环境相关——直接跑 TS 时生成的缓存对应 TS 文件,生产环境需用
mikro-orm cache:generate重新生成,或改用预构建缓存(详见 metadata-cache.md); - 新增实体后的两种选择:保持文件夹发现(零配置),或用
discovery:export生成 barrel 后交给打包器(更好的类型与性能特性)。
参考实现与测试入口
- 核心配置项定义:Configuration.ts(
entities/entitiesTs)、Configuration.ts(preferTs); - 发现流程主逻辑:MetadataDiscovery.ts(
findEntities); - 文件扫描与动态导入:discover-entities.ts;
- CLI 调试命令:DebugCommand.ts;
- CLI barrel 导出命令:DiscoveryExportCommand.ts;
- 相关测试:tests/MikroORM.test.ts、tests/features/cli/DebugCommand.test.ts、tests/features/cli/DiscoveryExportCommand.test.ts。
综上,文件夹发现是 MikroORM 7 面向大型项目提供的一等公民能力:以命名约定 + glob 模式换取零维护的实体注册,同时通过debug与discovery:export两个命令补齐了可观测性与类型安全,让开发者可以在"纯文件夹发现"与"生成式显式引用"之间按项目形态灵活选择。
- 后端
【免费下载链接】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 基于文件夹的实体自动发现(Folder-based Discovery)完整指南
MikroORM 基于文件夹的实体自动发现(Folder based Discovery)完整指南 导读 在 MikroORM 中,实体既可以像 entitie
后端MikroORM 文件夹式实体发现(Folder-based Discovery)完整指南
MikroORM 文件夹式实体发现(Folder based Discovery)完整指南 MikroORM 允许你通过 glob 模式自动发现实体,无需在配置
后端MikroORM Folder-based Discovery 完全指南:用 Glob 模式自动发现实体的原理与实战
MikroORM Folder based Discovery 完全指南:用 Glob 模式自动发现实体的原理与实战 导读 在 MikroORM 中,你既可以在
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考