☰
MikroORM 7.0 实体构造函数详解:em.create 参数推断、rel()/ref() 辅助函数与 forceEntityConstructor 配置
2026/9/25 3:17:59 网站建设 项目流程
  • 后端

【免费下载链接】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 中,实体构造函数只在"新建"场景下被调用:通过EntityManager从数据库加载的托管实体(managed entities)从不经过构造函数,因此你可以自由地把构造函数当作"数据守门人"使用。本文基于 v7.0 官方文档 Using Entity Constructors 并结合核心源码展开:你将掌握em.create()如何自动推断构造函数参数、如何用rel()/ref()在构造函数中把主键/POJO 安全地转换为实体引用,以及如何通过forceEntityConstructor开关解决 TS 原生私有字段(#x)带来的兼容问题。

核心原理:MikroORM 何时才调用实体构造函数?

官方文档开篇即给出关键结论:

内部实现上,MikroORM 从不对托管实体调用构造函数;构造函数只在你自己用new实例化(或使用em.create()创建新实体)时才会被执行,因此它是"创建新实体时强制要求必填数据"的理想位置。

从源码可以精确印证这一行为。在 EntityFactory.ts 的createEntity()私有方法中,实例的创建分为两条路径:

if (options.newEntity || meta.forceConstructor || meta.virtual) { const params = this.extractConstructorParams<T>(meta, data, options); const Entity = meta.class as Constructor<T>; // creates new instance via constructor as this is the new entity const entity = new Entity(...params); // ... return entity; } // creates new entity instance, bypassing constructor call as its already persisted entity const entity = Object.create(meta.class.prototype) as T; helper(entity).__managed = true;
  • 走new Entity(...)的条件是:创建新实体(newEntity)、实体被标记为强制使用构造函数(meta.forceConstructor,即下文forceEntityConstructor开关的结果),或为虚拟实体(virtual);
  • 其余情况——尤其是从数据库行数据水合(hydration)出来的托管实体——直接通过Object.create(meta.class.prototype)生成一个只继承了原型方法、不执行构造函数体的实例,然后把行数据逐字段挂载上去。

这意味着:构造函数里写的任何逻辑(参数校验、默认值计算、抛错)都不会在查询加载路径上触发,可以放心用于"创建时约束",而不会给find*系列查询带来性能或副作用开销。

用构造函数强制必填属性:完整的 Book 示例

以下Book实体定义要求title和author必填,而publisher可选:

@Entity() export class Book { @PrimaryKey() id!: number; @Property() title: string; @Property() foo!: number; @ManyToOne() author: Author; @ManyToOne() publisher?: Publisher; @ManyToMany({ entity: () => BookTag, inversedBy: 'books' }) tags = new Collection<BookTag>(this); constructor(title: string, author: Author) { this.title = title; this.author = author; } }

直接用new构造即可:

const author = new Author(); const book = new Book('Foo', author);

更关键的是,em.create()会自动检测并遵循构造函数签名:

const author = new Author(); const book = em.create(Book, { title: 'Foo', author, foo: 123 });

这一行会完成三件事:

  1. 从数据中抽取title与author两个键,作为参数传给new Book(title, author);
  2. 其余属性(示例中只有foo)不参与构造,而是由水合器(hydrator)赋值到实例上;
  3. 若实体已存在于身份映射(Identity Map)中,则直接返回已有实例并合并数据,不会重复走构造函数。

源码层面,em.create()进入 EntityFactory.create() 后,若命中"新建"分支,会在 L169-L172 先把构造函数参数从数据副本中剔除,再交给水合器处理剩余字段:

if (options.newEntity || meta.forceConstructor || meta.virtual) { const tmp = { ...data }; meta.constructorParams?.forEach(prop => delete tmp[prop as EntityKey<T>]); this.hydrate(entity, meta2, tmp, options); // ... }

而参数抽取由 extractConstructorParams() 完成。它按meta.constructorParams中记录的参数名逐一对应数据键;对多对一/一对一关系字段,它还会把裸主键值自动转换成实体引用(createReference),对嵌入对象调用createEmbeddable,对自定义类型执行convertToJSValue。构造函数参数名是如何确定的?以EntitySchema为例,元数据发现阶段通过 Utils.getConstructorParams(cls) 从类上解析出形参名并存入meta.constructorParams;在继承场景下,若子类未声明构造参数,会继承基类的构造参数列表。

重要约束:构造函数参数的推断基于实体属性名——你的形参名必须与实体属性名完全一致,em.create()才能把数据键正确映射到构造参数。

测试用例 constructor-params.test.ts 展示了带自定义Type的更复杂场景:User与Book的构造函数均声明了id(使用自定义IdentityType类型)等参数,em.create()在构造时会经过convertToJSValue转换后传入,验证了上述推断链路在自定义类型下同样成立。

构造函数中的 POJO vs 实体实例:rel() 与 ref() 辅助函数

实际业务中,构造函数接收的往往是 DTO(普通对象/主键),而不是 ORM 实体实例。文档指出两类典型陷阱:

  1. 类型层面直接报错:dto.author是number(主键),赋值给Author类型的关系属性会编译失败;
  2. 更隐蔽的 POJO 问题:dto.author是一个普通对象(POJO),可能通过类型检查,但运行时 ORM 关系属性只接受实体实例,POJO 不会工作——"ORM 期望关系属性中是实体实例,别无其他"。

用 rel() 把主键转成裸实体引用

rel()辅助函数可以无痛地把主键转换为实体引用:

@ManyToOne({ entity: () => Author }) author: Rel<Author>; constructor(dto: { title: string; author: number }) { this.title = dto.title; this.author = rel(Author, dto.author); }

rel()创建的实例此时尚未被管理(你没有传入任何EntityManager),但一旦其进入 ORM 管理范围,就会被当作一个"已存在的实体引用"处理。文档强调:这实际上等价于em.getReference(),只是不需要手边持有EntityManager实例;实现上,rel()就是Reference.createNakedFromPK()的快捷方式,见 Reference.ts:

export function rel<T, PK extends Primary<T>>(entityType: EntityClass<T>, pk?: T | PK): T | undefined | null { if (pk == null || Utils.isEntity(pk)) { return pk as T; } return Reference.createNakedFromPK(entityType, pk) as T; }

用 ref() 获得受 Reference 包装的安全版本

如果你希望更安全,让关系属性持有Reference包装(即ref: true模式),ref()也支持同样的"类型 + 主键"签名:

@ManyToOne({ entity: () => Author, ref: true }) author: Ref<Author>; constructor(dto: { title: string; author: number }) { this.title = dto.title; this.author = ref(Author, dto.author); }

ref() 的重载覆盖了三类用法:

  • ref(entity):实体实例的快捷方式,等价于wrap(entity).toReference();
  • ref(scalar):把标量值包装为ScalarReference;
  • ref(entityType, pk):等价于Reference.createFromPK(entityType, pk)。

rel与ref都同时接受主键、实体实例以及空值(null/undefined),文档给出了完整的取值组合示例:

book.author = ref(Author, null); book.author = ref(Author, undefined); book.author = ref(null); book.author = ref(undefined); book.author = ref(Author, 1); book.author = ref(Author, author); book.author = ref(author);

从源码看,这些空值/多态分支正是由 ref() 实现中的多重重载 保证的:首参为实体时走toReference(),单参非实体时包装为ScalarReference,双参时走createFromPK,null则原样返回。

原生私有字段与 forceEntityConstructor 开关

默认情况下,MikroORM 对已持久化实体使用Object.create()创建实例以绕过构造函数。这一策略与 TypeScript 原生私有属性(#private)不兼容:原生私有字段的访问受限类内部,Object.create()生成的"半成品"实例在访问这些字段时会抛错(该问题详见 v7.0 配置文档中引用的上游 issue)。

若你的实体需要原生私有字段,可以启用forceEntityConstructor开关强制所有实体实例都走构造函数。根据 v7.0 配置文档,该开关支持全局布尔或按实体粒度配置:

const orm = await MikroORM.init({ // ... forceEntityConstructor: true, // 或仅指定部分实体,如 [Author, 'Book', ...] });

源码印证了两点实现细节:

  1. 按实体粒度判断:MetadataDiscovery.shouldForceConstructorUsage() 会检查配置值——若是数组,则判断当前实体是否匹配列表中的类或类名,否则直接返回布尔值:
private shouldForceConstructorUsage<T>(meta: EntityMetadata<T>) { const forceConstructor = this.#config.get('forceEntityConstructor'); if (Array.isArray(forceConstructor)) { return forceConstructor.some(cls => Utils.matchesEntity(cls, meta)); } return forceConstructor; }
  1. 防止误更新:当开启强制构造函数后,从数据库加载实体也会执行构造函数。为了避免构造时设置的属性值在下次flush时产生无意义的 UPDATE,createEntity() 中有一处专门的清理逻辑:对"数据中未提供"的非主键持久属性,直接从实例上删除,使其保持undefined,从而不参与变更检测。

  2. 环境变量支持:该配置同样可通过环境变量MIKRO_ORM_FORCE_ENTITY_CONSTRUCTOR设置(见 configuration.md 的环境变量映射表),便于在 Docker 等容器化部署中不改动代码开启。

小结

  • MikroORM 对托管实体永远不执行构造函数,实例化走Object.create()路径;只有new与em.create()的新建场景才会触发构造函数,因此它天然适合作为"创建时必填数据"的校验点;
  • em.create()基于形参名与实体属性名完全一致的约定自动推断构造参数(由 extractConstructorParams 实现),其余字段交给水合器赋值;
  • 构造函数内收到主键或 POJO 时,用rel()生成裸实体引用(等价em.getReference()语义),或用ref()生成受Reference包装的安全引用,两者均兼容主键、实体实例与空值;
  • 需要 TS 原生私有字段时,用forceEntityConstructor(布尔或实体数组,亦可经MIKRO_ORM_FORCE_ENTITY_CONSTRUCTOR环境变量注入)强制走构造函数,源码已内置清理逻辑避免加载路径产生误更新。
  • 后端

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

相关推荐

上一篇:Realm Gradle插件深度解析:构建Android数据库架构的强力引擎
下一篇:Vim从入门到精通:Vim与Neovim功能对比

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

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

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

立即咨询