后端【免费下载链接】mikro-ormTypeScript 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 v6.6 的官方指南《Type-safety》为基础系统讲解实体关系在类型系统层面的安全访问方案。你将掌握Reference/Ref包装器的语义与 API、LoadedT, P类型如何随populate与fields提示自动收窄、$符号与get()的同步访问技巧以及如何把部分加载partial loading严格到属性级。文中的全部代码示例均可直接复制到使用 v6 系列的 TypeScript 项目中运行验证。一、实体关系默认的“未加载”语义MikroORM 采用 Data Mapper 模式实体关系在数据库中通常只存储外键。加载一个实体时其ManyToOne、OneToOne关系默认不会跟着查询出来而是被映射为一个实体引用entity reference——一个只含有主键的实体实例。这个引用会被存放在 Identity Map 中因此从数据库重复获取同一份文档时你会拿到同一个对象引用详见 identity-map.md。ManyToOne(() User) author!: User; // 运行时的值始终是 User 实体的实例这带来一个经典陷阱类型系统以为author是完整的User但运行时它只是一个“壳”const article await em.findOne(Article, 1); console.log(article.author instanceof User); // true确实是一个 User 实例 console.log(wrap(article.author).isInitialized()); // false但并未加载 console.log(article.author.name); // undefined因为 User 尚未加载1.1 判断初始化状态wrap(entity).isInitialized()通过wrap()WrappedEntity辅助方法见 entity-helper.md 中的WrappedEntity与wraphelper 章节可以检查实体是否已初始化并惰性加载它const user em.getReference(User, 123); console.log(user.id); // 打印 123访问主键不会触发任何数据库调用 console.log(wrap(user).isInitialized()); // false它只是一个引用 console.log(user.name); // undefined await wrap(user).init(); // 这会触发数据库调用 console.log(wrap(user).isInitialized()); // true console.log(user.name); // 已定义isInitialized()可用于运行时检查但到处手写检查既繁琐又容易遗漏。MikroORM 提供了更优雅的方案Reference包装器把“未加载”状态直接表达进类型系统。二、Reference包装器把懒加载表达进类型2.1 问题根源编译器以为关联总是已加载ManyToOne/OneToOne属性被声明为实体类型时TypeScript 编译器会认为该实体总是已加载于是article.author.name这样的访问在编译期不会报错运行时却返回undefined——类型安全在这里失效了。ReferenceT包装器解决了这个问题它包住实体提供load(): PromiseT方法如尚未加载则先惰性加载关联并提供unwrap(): T直接访问底层实体而不触发加载。另有loadK extends keyof T(prop: K): PromiseT[K]与load()类似但直接返回指定属性。import { Entity, Ref, ManyToOne, PrimaryKey, Reference } from mikro-orm/core; Entity() export class Article { PrimaryKey() id!: number; // 本指南使用 ts-morph 元数据提供器因此这样写就够了。 ManyToOne() author: RefUser; constructor(author: User) { this.author ref(author); } }使用效果一目了然const article1 await em.findOne(Article, 1); article.author instanceof Reference; // true article1.author; // RefUserReference 类的实例 article1.author.name; // 类型错误不存在 name 属性 article1.author.unwrap().name; // 不安全的同步访问author 未加载时为 undefined article1.author.isInitialized(); // false const article2 await em.findOne(Article, 1, { populate: [author] }); article2.author; // LoadedReferenceUserReference 类的实例 article2.author.$.name; // 类型安全的同步访问2.2 同步 gettergetEntity()与getProperty()Reference还提供两个同步gettergetEntity()与getProperty()。它们会先检查被包装实体是否已初始化若未初始化则直接抛错把“未加载”的运行时错误提前到访问点const article await em.findOne(Article, 1); console.log(article.author instanceof Reference); // true console.log(wrap(article.author).isInitialized()); // false console.log(article.author.getEntity()); // Error: ReferenceUser 123 not initialized console.log(article.author.getProperty(name)); // Error: ReferenceUser 123 not initialized console.log(await article.author.load(name)); // ok先加载 author console.log(article.author.getProperty(name)); // okauthor 已加载在源码层面packages/core/src/entity/Reference.tsgetEntity()的实现正是先经isInitialized()检查再抛错getPropertyK extends keyof T(prop: K)则委托给getEntity()[prop]load(prop)的内部实现则是先loadOrFail()再取属性。此外Reference还提供了loadOrFail()实体不存在时按findOneOrFail语义抛错、populated()控制序列化时的已填充标记与toJSON()等能力。2.3 不同元数据提供器下的ref: true如果你使用的不是TsMorphMetadataProvider例如ReflectMetadataProvider则还需要显式设置ref参数ManyToOne(() User, { ref: true }) author!: RefUser;在核心实现中Reference.wrapReference() 会在属性声明了ref且值还不是Reference时自动用Reference.create(entity)把它包起来并绑定属性元数据——这也是hydrate流程中Ref属性自动生效的底层机制。2.4 使用Reference.load()按需加载取到引用后可用异步的Reference.load()加载完整实体const article1 await em.findOne(Article, 1); (await article1.author.load()).name; // 异步安全访问 const article2 await em.findOne(Article, 2); const author await article2.author.load(); author.name; await article2.author.load(); // 不会发起额外查询因为已加载与wrap(e).init()总是刷新实体不同Reference.load()只有在实体尚未存在于 Identity Map 中时才会查询数据库。从 load() 的实现看若实体已初始化且未要求refresh它会直接返回现有实体仅在未初始化或要求刷新时才真正触发init()或走 dataloader 批处理路径。三、ScalarReference包装器懒加载标量属性Reference同样适用于标量把标量包进Ref就得到ScalarReference对象常用于懒加载标量属性lazy属性。Ref类型对非对象类型会自动解析为ScalarReference因此下面的写法是自洽的Property({ lazy: true, ref: true }) passwordHash!: Refstring;const user await em.findOne(User, 1); const passwordHash await user.passwordHash.load();对于对象类型的属性若也想用引用包装器应显式使用ScalarRefT类型。例如想懒加载一个较大的 JSON 值Property({ type: json, nullable: true, lazy: true, ref: true }) // ReportParameters 是对象类型假定定义在别处。 reportParameters!: ScalarRefReportParameters | null;需要特别注意一旦标量值通过ScalarReference管理通过 MikroORM 托管对象访问该属性时总是返回ScalarReference包装器。若属性同时又是nullable会带来一个反直觉的结果——ScalarReference本身始终为真值truthy。此时应通过ScalarReferenceT的类型参数把可空性告知类型系统如上面的ScalarRefReportParameters | null。完整示例// 假设数据库中的 Report(id1) 没有 reportParameters。 const report await em.findOne(Report, 1); if (report.reportParameters) { // 这里打印的是 Ref? 而不是真实值。**这段代码永远会执行**。 console.log(report.reportParameters); // ts-expect-error 在引用加载完成前$/.get() 不可用。 // const mistake report.reportParameters.$ } const populatedReport await em.populate(report, [reportParameters]); // 打印 null console.log(populatedReport.reportParameters.$);从源码看packages/core/src/entity/Reference.ts#L256-L341ScalarReference维护内部#initialized状态load()在未初始化时通过helper(entity).populate([property])触发加载bind()把它与宿主实体和属性名绑定unwrap()返回当前标量值未加载时为undefined。四、Loaded类型让 populate 提示参与类型推导4.1em.find/em.findOne的返回类型查看em.find与em.findOne的返回类型你会发现返回的不是裸实体而是Loaded类型// res1 的类型是 LoadedUser, never[] const res1 await em.find(User, {}); // res2 的类型是 LoadedUser, identity | friends[] const res2 await em.find(User, {}, { populate: [identity, friends] });假设User实体定义如下import { Entity, PrimaryKey, ManyToOne, OneToOne, Collection, Ref, ref } from mikro-orm/core; Entity() export class User { PrimaryKey() id!: number; ManyToOne(() Identity) identity: RefIdentity; ManyToMany(() User) friends new CollectionUser(this); constructor(identity: Identity) { this.identity ref(identity); } }Loaded类型会记录实体的哪些关系已被填充populated并为它们附加一个特殊的$符号从而允许对已加载属性做类型安全的同步访问。这与Reference包装器配合得非常好如果不喜欢$这种魔法符号也可以使用get()方法——它是$的别名。// res 的类型是 LoadedUser, identity const user await em.findOneOrFail(User, 1, { populate: [identity] }); // 无需 await user.identity.load() 这种异步调用 // 直接通过动态添加的 $ 符号做同步且类型安全的访问 console.log(user.identity.$.email);如果省略populate提示user的类型会退化为LoadedUser, neveruser.identity.$符号便不可用——这类调用会直接产生编译错误// 不加 populate 提示时类型是 LoadedUser, never const user2 await em.findOneOrFail(User, 2); // TS2339: Property $ does not exist on type { id: number; } Reference. console.log(user.identity.$.email);4.2Collection上的$符号同样的机制也作用于Collection包装器——它提供运行时方法isInitialized、loadItems、init以及类型安全的$符号// res 的类型是 LoadedUser, friends const user await em.findOneOrFail(User, 1, { populate: [friends] }); // 无需 await user.friends.loadItems() // 直接遍历动态添加的 $ 符号 for (const friend of user.friends.$) { console.log(friend.email); }4.3 在自定义方法中利用LoadedLoaded类型还可以用在自定义函数签名里从类型层面强制要求某些关系必须被填充function checkIdentity(user: LoadedUser, identity) { if (!user.identity.$.email.includes()) { throw new Error(Thats a weird e-mail!); } }// 通过编译 const u1 await em.findOneOrFail(User, 2, { populate: [identity] }); checkIdentity(u1); // 编译失败 const u2 await em.findOneOrFail(User, 2); checkIdentity(u2);请注意这一切都只是类型层面的信息可以通过类型断言type assertion轻易绕过。从类型实现看packages/core/src/typings.ts#L2702-L2710LoadedT, L, F, E由内部LoadedInternal根据字段提示F分派全字段*时遍历T的属性仅对命中populate提示L含AddEager扩展的属性套上LoadedProp指定了fields时则退化为SelectedT, L, F。而 LoadedReference / LoadedCollection / LoadedScalarReference 接口 分别给Reference、Collection、ScalarReference补上了$与get()声明。运行时$与get()通过 Reference.prototype 上的 getter 直接返回被包装的实体/值。五、严格部分加载Strict partial loadingLoaded类型同样会尊重部分加载提示fields选项。一旦使用fields返回类型的可访问属性会被限制为被选中的属性主键会被自动选中并保留在类型层面。// article 的类型是 SelectedArticle, author, title | author.email const article await em.findOneOrFail(Article, 1, { fields: [title, author.email], populate: [author], }); const id article.id; // ok主键自动选中 const title article.title; // oktitle 被选中 const publisher article.publisher; // fail未被选中 const author article.author.id; // ok主键自动选中 const email article.author.email; // ok被选中 const name article.author.name; // fail未被选中对应地SelectedT, L, F类型 会按L | F提示对T的属性做过滤函数属性原样保留标量属性保持原样非标量属性则交给LoadedProp递归处理并保留__fieldsHint标记供后续类型检查使用。官方还提供了可交互的实时演示MikroORM v6 严格部分加载的 StackBlitz 示例mikro-orm-v6-strict-partial-loading其中basic.test.ts以测试的形式演示了上述行为可直接在浏览器中运行验证。六、向Reference属性赋值6.1 三种赋值方式属性被声明为Reference包装器后赋值时就要赋Reference实例而非裸实体。可以通过ref(entity)把任意实体转成Reference或使用em.getReference()的wrapped选项ref(e)是wrap(e).toReference()的简写等价于Reference.create(e)。import { ref } from mikro-orm/core; const article await em.findOne(Article, 1); const repo em.getRepository(User); article.author repo.getReference(2, { wrapped: true }); // 与上面等价 article.author ref(repo.getReference(2)); await em.flush();从 EntityManager.getReference() 的重载签名可见传入{ wrapped: true }时返回类型为RefEntity不传时返回Entity其注释明确说明“在不实际加载实体的前提下获取引用若实体尚未加载”。ref()工具函数packages/core/src/entity/Reference.ts#L409-L449的运行时逻辑则是对实体调用helper(e).toReference()对“实体类型 主键”调用Reference.createFromPK()单参数非实体值则包成ScalarReference。6.2 在实体构造函数中创建引用v5自 v5 起可以在不访问EntityManager的情况下创建实体引用——例如在实体构造函数内部。这借助rel()辅助函数与Rel类型import { Entity, ManyToOne, Rel, rel } from mikro-orm/core; Entity() export class Article { ManyToOne(() User, { ref: true }) author!: RefUser; constructor(authorId: number) { this.author rel(User, authorId); } }rel(entityType, pk)是Reference.createNakedFromPK(entityType, pk)的简写packages/core/src/entity/Reference.ts#L512-L523内部通过实体类型的__factory创建“裸”引用不包装成Reference也不触发数据库查询。6.3 通过toReference()赋值也可以使用WrappedEntity接口提供的toReference()方法详见 entity-helper.md 的 “WrappedEntity 与 wrap helper” 章节const author new User(...) article.author wrap(author).toReference();6.4 已存在引用的替换引用reference与实体一样持有身份identity因此替换已有引用时需要用新的Reference实例重新赋值article.author ref(new User(...));七、Ref类型到底是什么Ref是一个交叉类型它在Reference接口的基础上追加了主键属性从而可以直接从Reference实例上取主键。默认情况下MikroORM 会按顺序探测以下已知主键属性名_id、uuid、id也可以通过PrimaryKeyProp符号手动指定属性名[PrimaryKeyProp]?: foo;。const article await em.findOne(Article, 1); console.log(article.author.id); // ok返回主键从类型定义看packages/core/src/typings.ts#L863-L881RefT会按T是否为标量解析为ScalarReferenceT或EntityRefTEntityRef 主键属性交叉ReferenceT运行时Reference 构造函数则会遍历元数据中的primaryKeys为每个主键在包装器实例上定义 getter 直接转发到被包装实体并额外处理serializedPrimaryKey的转发。八、关联阅读与可运行验证关系映射与Ref的更多用法relationships.md、populating-relations.mdwrap()/WrappedEntity完整 APIentity-helper.mdIdentity Map 与实体引用语义identity-map.md元数据提供器差异决定是否需要ref: truemetadata-providers.md懒加载标量属性Property({ lazy: true })的声明方式见 defining-entities.md核心源码Reference/ScalarReference/ref()/rel()/unref()位于 packages/core/src/entity/Reference.tsLoaded/Selected/Ref/LazyRef/ScalarRef等类型位于 packages/core/src/typings.ts本文所有示例均基于当前仓库 v6.6 分支的文档与源码编写若你正在使用 v7可对照 version-7.2 的对应指南 查看差异。将Reference包装器、Loaded类型与严格部分加载组合使用可以让“关系是否已加载”这一运行时事实在编译期就被类型系统完整约束从根源上避免undefined属性访问这类常见 Bug。赞分享后端【免费下载链接】mikro-ormTypeScript 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 类型安全关系Ref、Reference、ScalarReference 与 Loaded 类型实战指南MikroORM 类型安全关系Ref、Reference、ScalarReference 与 Loaded 类型实战指南 在 MikroORM 中实体关系映后端dokploy类型安全TypeScript严格模式与类型定义dokploy类型安全TypeScript严格模式与类型定义 引言为什么类型安全对现代部署平台至关重要 在现代云原生应用部署领域类型安全不再是一个可选项后端前端云原生DevOps容器编排运维freeCodeCamp类型安全TypeScript严格模式与全类型覆盖的工程实践freeCodeCamp类型安全TypeScript严格模式与全类型覆盖的工程实践 在大型开源项目中类型安全是保障代码质量和开发效率的关键支柱。freeCo前端后端教育上一篇解决Lucide React模块系统兼容性问题从动态导入到TypeScript类型适配下一篇使用 Rube MCP 在 Codex 中自动化 Reply.io 邮件外展工作流awesome-codex-skills 的 reply-io-automation 技能实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
