☰
MikroORM 属性校验(Property Validation)完全指南:必填属性、OptionalProps、Opt 与运行时校验
2026/9/26 6:42:39 网站建设 项目流程
  • 后端

【免费下载链接】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 中负责“实体属性是否必须提供”的一套机制,它在 TypeScript 类型层面(编译期)与运行时层面(flush 阶段)双轨工作:一方面通过RequiredEntityData、OptionalProps、Opt等类型约束让em.create()/em.assign()在编译期就提示必填项;另一方面在flush()执行 INSERT 前,由 ChangeSetPersister 逐属性校验缺失值并抛出ValidationError。本篇指南将完整讲解必填/可空属性的声明方式、带默认值属性的类型提示问题、OptionalProps符号与Opt类型的使用场景,以及如何通过validateRequired: false关闭运行时校验,帮助你写出类型安全且运行时行为可预期的实体定义。

必填属性与可空属性

在 MikroORM 中,实体属性默认被视为必填(required)。这意味着每个属性都会经历两层校验:

  • 类型层面(编译期):em.create()等 API 的参数类型会基于实体元数据推导,必填属性必须出现在入参中,否则 TypeScript 直接报错;
  • 运行时层面:在flush()发出 INSERT 之前,ORM 会检查实体内必填属性是否有值,缺失则抛出ValidationError。

要让一个属性成为可空的,需要在类型层面和元数据层面同时标记。除非你使用ts-morph做元数据反射,否则两者缺一不可:

@Property({ nullable: true }) name?: string;

如果你希望属性类型是显式的null联合(即允许赋null),还应提供属性初始化器:

@Property({ type: 'string', nullable: true }) name: string | null = null;

注意nullable: true在元数据上的影响不止于校验:它同时决定数据库列的 NULL 约束、TypeScript 推导出的可空性,以及序列化时的行为。从 typings.ts 的Nullify<P, V>类型可以看出,nullable: true会在推导结果上追加| null。

必填校验的运行时实现

运行时校验发生在flush()期间、INSERT 查询发出之前。核心实现位于 ChangeSetPersister.ts 的validateRequired方法,它遍历实体元数据的所有属性,仅在满足以下全部条件时才认为属性“需要值”并执行空值检查:

  • 属性不是nullable;
  • 不是自增主键(autoincrement);
  • 没有default/defaultRaw/onCreate值;
  • 不是生成列(generated);
  • 不是嵌入式属性(embedded);
  • 不是ONE_TO_MANY/MANY_TO_MANY这类集合关系;
  • 不是全由公式、非持久化或主键组成的嵌入式目标;
  • 不是继承体系中的判别列(discriminatorColumn);
  • 类型不是ObjectId;
  • persist !== false。

从 errors.ts 可以看到抛出的异常信息非常具体:Value for Author2.email is required, 'undefined' found,并附带整个实体的inspect快照,方便快速定位是哪个实体的哪个属性缺失。

校验只在ChangeSetType.CREATE(新建)场景触发,更新(UPDATE)不会要求全部必填属性都有值:

if (changeSet.type === ChangeSetType.CREATE && this.#config.get('validateRequired')) { this.validateRequired(changeSet.entity); }

带默认值属性的类型处理

运行时校验对“有默认值的必填属性”没有意见——只要默认值存在,flush 时该属性一定有值,校验自然通过。真正的难点在类型层面:属性在 TS 中被定义为必填(没有?),但因为有默认值,它实际上又是可选的(调用方可以不传)。直接em.create()时 TypeScript 会要求你显式传入该属性,这并不理想。

MikroORM 提供三种解法:

方案一:把属性定义为可选(不推荐)

@Property({ default: 1 }) level?: number = 1;

这样做虽然类型通过了,但副作用是允许外部把该属性显式“置空/取消”(unset),这可能并不是你想要的行为。

方案二:使用OptionalProps符号(推荐)

OptionalProps是 MikroORM 导出的一个Symbol(定义见 typings.ts),专门用于解决“属性有默认值但希望类型上可选”的问题。它的用法是:在实体上声明一个可选属性[OptionalProps]?: 'propA' | 'propB' | ...,值的类型是你要标记为可选的所有属性名的联合类型:

import { OptionalProps, Entity, PrimaryKey, Property } from '@mikro-orm/core'; @Entity() class User { // getters 也会遇到同样的问题,需要一并声明 [OptionalProps]?: 'foo' | 'bar' | 'fooBar'; @PrimaryKey() id!: number; @Property({ default: 1 }) foo: number = 1; @Property({ default: 2 }) bar: number = 2; @Property({ persist: false }) get fooBar() { return foo + bar; } }

从源码看,ExplicitlyOptionalProps<T>会同时收集[OptionalProps]声明的键,以及所有类型为Opt的属性键;随后RequiredEntityData<T>在推导em.create()入参时,会把这些键归入“可选”分支,从而在编译期放行“不传默认值属性”的调用。

注意注释中强调:getter 属性(如示例中的fooBar)同样需要列入OptionalProps,因为它们在构造实体时不可能由调用方提供。

方案三:在基类中用泛型扩展 OptionalProps

当你把公共的默认值属性下沉到自己的 BaseEntity 时,需要借助泛型,让子类可以继续追加自己的可选属性:

@Entity() class MyBaseEntity<Entity extends object, Optional extends keyof Entity = never> { [OptionalProps]?: 'foo' | 'bar' | Optional; @PrimaryKey() id!: number; @Property({ default: 1 }) foo: number = 1; @Property({ default: 2 }) bar: number = 2; } @Entity() class User extends MyBaseEntity<User, 'baz'> { @Property({ default: 3 }) baz: number = 3; }

这里Optional extends keyof Entity = never是关键:子类实例化时把自己的类型User作为第一个泛型参数传入,并把新增的可选属性名'baz'作为第二个参数,从而把'baz'并入基类已经声明的'foo' | 'bar'联合中。

方案四:Opt类型

Opt是另一种更轻量的选择,它是一个品牌类型(branded type),定义于 typings.ts,声明为Opt<T> = T & Opt.Brand。它有两种等价的用法:

  • 泛型形式:middleName: Opt<string> = '';
  • 交叉类型形式:middleName: string & Opt = '';

两种写法效果相同,且可以与OptionalProps符号方案组合使用:

import { Opt, Entity, PrimaryKey, Property } from '@mikro-orm/core'; @Entity() class User { @PrimaryKey() id!: number; @Property() firstName!: string; @Property() middleName: string & Opt = ''; @Property() lastName!: string; @Property({ persist: false }) get fullName(): Opt<string> { return `${this.firstName} ${this.middleName} ${this.lastName}`; } }

Opt尤其适合 getter、persist: false派生属性或无法用[OptionalProps]清晰表达的场景;而[OptionalProps]符号则更适合集中声明“一组”默认值属性。二者在类型推导路径上殊途同归——都会进入ProbablyOptionalProps<T>的可选判定。

运行时校验的开关:validateRequired

如果你出于某些原因不希望 ORM 在缺失必填属性时抛错,可以关闭运行时校验:

// MikroORM.init 配置 const orm = await MikroORM.init({ entities: [...], validateRequired: false, }); // 或在运行时切换 orm.config.set('validateRequired', false);

该配置项默认值为true(见 Configuration.ts 的默认配置),属于全局配置。

关闭校验后,缺失必填属性的错误将从 ORM 层转移到数据库层——这一点有明确的测试佐证。在 EntityManager.postgre.test.ts 的required fields validation测试中:

  • 默认开启时,flush()抛出Value for Author2.email is required, 'undefined' found;
  • 关闭validateRequired后,同样操作抛出的是数据库的null value in column "email" of relation "author2" violates not-null constraint,即NotNullConstraintViolationException。

这意味着:关闭校验并不能“绕过”必填约束,只是把检查时点与报错形态从 ORM 的友好提示换成了数据库的约束异常。生产环境建议保持默认开启,以获得更快、更可读的失败反馈。

关于可选属性与元数据反射的注意事项

定义实体时,可选属性需要特别小心,这与元数据提供器(metadata provider)的能力边界有关:

  • 使用默认的reflect-metadata提供器时,属性类型只能通过?后缀(可选标记)推断。如果你使用联合类型如string | null,reflect-metadata无法解析这种复杂类型,此时你必须显式声明类型(例如@Property({ type: 'string', nullable: true }));
  • 这个问题在使用ts-morph提供器时不存在,因为它直接读取 TypeScript AST,可以理解string | null这类联合类型。

这正是本文开头示例中@Property({ type: 'string', nullable: true })需要显式给出type的原因。若你的项目大量使用nullable联合类型,且不愿到处手写类型,可以考虑切换到ts-morph提供器(配置项为metadataProvider: TsMorphMetadataProvider)。

综合示例:完整的必填/可选属性实体

将以上要点整合,一个兼顾类型安全与运行时行为的实体大致长这样:

import { Entity, PrimaryKey, Property, OptionalProps, Opt } from '@mikro-orm/core'; @Entity() class Account { [OptionalProps]?: 'createdAt' | 'updatedAt'; @PrimaryKey() id!: number; @Property() email!: string; // 必填,类型与运行时双重校验 @Property({ nullable: true }) displayName?: string; // 可空 @Property({ type: 'string', nullable: true }) bio: string | null = null; // 显式 null 联合 + 初始化器 @Property({ default: 1 }) level: number = 1; // 有默认值,通过 OptionalProps 标记为类型可选 @Property() get label(): Opt<string> { return `${this.email} (${this.level})`; } @Property({ onCreate: () => new Date() }) createdAt: Date = new Date(); // 数据库/ORM 自动生成 @Property({ onUpdate: () => new Date() }) updatedAt: Date = new Date(); }

关键设计点回顾:

  1. 必填:不加nullable、不给默认值、不列入OptionalProps的属性,在em.create()类型检查与 flush 运行时检查中都会被强制要求;
  2. 可空:必须同时满足类型层面(?或| null+ 初始化器)与元数据层面(nullable: true);
  3. 默认值:运行时校验自动放行,类型层面用[OptionalProps]或Opt声明为可选;
  4. 关闭校验:仅当确实需要把错误下推给数据库时才设置validateRequired: false。

小结

属性校验是 MikroORM“类型安全优先”设计哲学的典型体现:编译期由 typings.ts 中的RequiredEntityData、OptionalProps、Opt等类型负责,运行时由 ChangeSetPersister.ts 在 INSERT 前兜底,配置项validateRequired(默认true,定义于 Configuration.ts)控制总开关。正确使用nullable: true、OptionalProps与Opt,可以让实体定义在“允许省略”与“防止遗漏”之间取得最佳平衡;而理解reflect-metadata与ts-morph提供器的差异,则能避免在可空联合类型上踩坑。相关行为均有测试覆盖,例如 EntityManager.postgre.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
点击查看免费下载

相关推荐

上一篇:CherryPy测试策略:单元测试、集成测试与性能测试
下一篇:Rust机器学习生态系统项目教程

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

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

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

立即咨询