MikroORM 7 中的 defineEntity 编程式实体定义指南

发布时间:2026/9/26 19:00:29
MikroORM 7 中的 defineEntity 编程式实体定义指南 后端【免费下载链接】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点击查看免费下载defineEntity是 MikroORM 7 官方推荐的编程式实体定义方式它构建在EntitySchema之上借助 TypeScript 类型推断自动生成实体类型无需装饰器decorators即可完成实体建模。本文以 docs/versioned_docs/version-7.1/define-entity.md 为核心骨架结合 packages/core/src/entity/defineEntity.ts 源码与 tests/defineEntity.test.ts 测试用例进行纵深讲解。读完后你将掌握defineEntity class推荐模式、纯defineEntity用法、基类属性复用、钩子注册以及底层EntitySchema低层 API 的完整实战方案。为什么选择 defineEntityMikroORM 的实体定义有两个主流方向基于装饰器Entity()、Property()等见 defining-entities.md与基于EntitySchema的编程式定义。defineEntity属于后者但它把EntitySchema的使用体验提升了一个档次零装饰器依赖实体定义是纯 TypeScript 对象适合 vanilla JavaScript 项目、代码生成场景以及需要避免reflect-metadata的环境类型自动推断从properties映射中直接推断出实体类型无需手写IBook之类的接口这正是defineEntity相对原始EntitySchema的核心优势建立在EntitySchema之上defineEntity最终返回的仍是一个EntitySchema实例文档原话是 “It returns anEntitySchemainstance with full type information”因此它与 ORM 发现discovery、Unit of Work、Identity Map 等机制完全兼容。从源码看defineEntity的实现位于 packages/core/src/entity/defineEntity.ts它把用户传入的properties映射中的每个 builder 通过getBuilderOptions()提取出底层选项对象再交给new EntitySchema({ properties, ...options })构造。对于函数形式的属性如() p.manyToOne(Author)源码用Object.defineProperty定义了惰性 getter——只有真正读取该属性时才执行 builder 函数并缓存结果这既支持了循环引用关系可以后向引用尚未定义的实体又避免了重复构建开销。快速上手定义第一个实体defineEntity从mikro-orm/core导入配合属性构建器快捷方式p使用import { defineEntity, p } from mikro-orm/core; const BookSchema defineEntity({ name: Book, properties: { id: p.integer().primary(), title: p.string(), author: () p.manyToOne(Author).inversedBy(books), tags: () p.manyToMany(BookTag).inversedBy(books).fixedOrder(), }, }); export class Book extends BookSchema.class {} BookSchema.setClass(Book);要点说明p是defineEntity.properties的别名源码中二者等价defineEntity.properties propertyBuilders; export { propertyBuilders as p }defineEntity.ts关系属性使用函数形式() p.manyToOne(...)这是因为关系目标Author、BookTag可能尚未定义完成延迟求值避免循环依赖.inversedBy(books)声明关系的反向端.fixedOrder()让m:n集合保持固定顺序对应源码fixedOrder(fixedOrder?: boolean)defineEntity.tsBookSchema.class是defineEntity自动生成的实体类导出并setClass注册后即可作为实体类型使用。对应测试用例见 tests/defineEntity.test.tsshould define entity其中验证了属性推断、关系构建与 ORM 集成行为。defineEntity class模式推荐文档明确指出当你继承自动生成的类并通过setClass()注册时能获得四项收益干净的 hover 类型悬停Book变量显示的是Book本身而不是带泛型与符号的复杂交叉类型更好的性能真正的具名类比纯模式动态生成的匿名类更高效自定义方法可以直接在实体实例上编写领域逻辑无需任何变通无属性重复属性只在 schema 中定义一次自动被类继承。完整示例const AuthorSchema defineEntity({ name: Author, properties: { id: p.integer().primary(), firstName: p.string(), lastName: p.string(), books: () p.oneToMany(Book).mappedBy(author), }, }); class Author extends AuthorSchema.class { fullName() { return ${this.firstName} ${this.lastName}; } } AuthorSchema.setClass(Author); // Usage: const author em.create(Author, { firstName: John, lastName: Doe }); console.log(author.fullName()); // John Doe这里oneToMany是1:m关系的反向端必须配合.mappedBy(author)指向拥有侧的m:1属性。重要setClass()必须在 ORM 发现过程运行之前调用即在MikroORM.init()之前。请在模块加载时、紧跟在扩展类定义之后调用它。从源码看setClass()会更新EntityMetadata的class、prototype、className并把实体注册进EntitySchema.REGISTRY以支持按类查找 schema同时它还会自动推断extends——只有当父类不是该实体自身的自动生成类时才把父类记录为基类EntitySchema.ts。这就是为什么“先 extends 自动类、再 setClass 注册”不会把自动生成的匿名类误判成独立的父实体。纯defineEntity无 class如果实体足够简单也可以不扩展类直接使用defineEntity。代价是 hover 时会出现复杂的计算类型import { type InferEntity, defineEntity, p } from mikro-orm/core; export const Book defineEntity({ name: Book, properties: { id: p.integer().primary(), title: p.string(), author: () p.manyToOne(Author).inversedBy(books), tags: () p.manyToMany(BookTag).inversedBy(books).fixedOrder(), }, }); // Use InferEntity to extract the entity type export type IBook InferEntitytypeof Book;无类模式下InferEntitytypeof Book用于提取实体类型如钩子参数、Loaded约束等场景。创建新实例时使用em.create()它会创建内部生成类的实例const book em.create(Book, { title: My Book, author }); await em.flush();InferEntityFromProperties是类型推断的核心工具类型defineEntity.ts它解析每个属性的 builder、合并基类属性与主键并附加PrimaryKeyProp、EntityRepositoryType等符号标记从而构造出完整的实体类型。复用基类属性跨实体共享公共属性有两种官方途径组合composition与继承extends。方式一组合共享属性对象最简单的方式——把共享属性对象展开进每个实体的propertiesconst p defineEntity.properties; const baseProperties { id: p.integer().primary(), createdAt: p.datetime().onCreate(() new Date()), updatedAt: p.datetime() .onCreate(() new Date()) .onUpdate(() new Date()), }; const BookSchema defineEntity({ name: Book, properties: { ...baseProperties, title: p.string(), author: () p.manyToOne(Author), }, }); export class Book extends BookSchema.class {} BookSchema.setClass(Book);其中.onCreate(cb)在 flush 创建实体时自动执行回调设置属性值源码注释Automatically set the property value when entity gets created, executed during flush operationdefineEntity.ts.onUpdate(cb)在每次实体更新时自动更新属性值defineEntity.ts回调签名(entity: any, em: EntityManager) ...可返回单值或数组。方式二extends继承与属性初始化器defineEntity class模式配合extends时自动生成的子类在 JavaScript 层面继承父类——基类上定义的属性初始化器如id v4()或createdAt new Date()会在通过new构造子实体时自动执行const BaseSchema defineEntity({ name: BaseEntity, abstract: true, properties: { id: p.string().primary(), createdAt: p.datetime(), updatedAt: p.datetime(), }, }); export class Base extends BaseSchema.class { id v4(); createdAt new Date(); updatedAt new Date(); } BaseSchema.setClass(Base); const UserSchema defineEntity({ name: User, extends: BaseSchema, properties: { email: p.string().unique(), name: p.string(), }, }); export class User extends UserSchema.class { name ; } UserSchema.setClass(User); // id, createdAt, updatedAt are initialized from Bases property initializers const user new User(); console.log(user.id); // a UUID string console.log(user.createdAt); // current Date该方案适用于“构造函数级默认值”——即不依赖EntityManager上下文、用普通new也能生效的场景。如果只需要持久化时的默认值优先使用onCreate钩子详见 inheritance-mapping.md。继承基类方法基类上声明的方法在运行时始终会被继承——自动生成的子类 extends 基类因此子实例是基类的instanceof且能调用其方法。但在类型层面extends: BaseSchema只携带映射属性TypeScript 看不到之后通过setClass附加到 schema 的方法。要让这些方法在子实体类型上可见应让extends指向基类class而非 schemaexport class Base extends BaseSchema.class { id v4(); createdAt new Date(); updatedAt new Date(); wasUpdated(): boolean { return this.updatedAt this.createdAt; } } BaseSchema.setClass(Base); const UserSchema defineEntity({ name: User, extends: Base, // the class, not BaseSchema — exposes wasUpdated() on the child type properties: { email: p.string().unique(), }, }); export class User extends UserSchema.class { describe() { return this.wasUpdated() ? ${this.email} (edited) : this.email; } } UserSchema.setClass(User);两种形式运行时行为完全一致传入 class 只是让 TypeScript 传播继承的方法签名。当基类仅贡献列columns时extends: BaseSchema仍是正确的选择。测试用例should inherit property initializers from parent class via extendstests/defineEntity.test.ts直接验证了这一行为子实体通过new构造后id、createdAt、updatedAt均来自父类属性初始化器。属性类型总览defineEntity.properties别名p提供了所有 MikroORM 内置类型完整列表见 custom-types.md。使用自定义类型时通过p.type()接入const properties { string: p.string(), float: p.float(), boolean: p.boolean(), json: p.json{ foo: string; bar: number }().nullable(), stringArray: p.type(ArrayTypestring).nullable(), numericArray: p.type(new ArrayType(i i)).nullable(), point: p.type(PointType).nullable(), };补充说明依据源码 defineEntity.tsp.string()使用StringPropertyOptionsBuilder其余大多数类型使用通用的UniversalPropertyOptionsBuilderp.bigint(mode?)支持bigint | number | string三种模式默认bigintp.decimal(mode?)支持number | string默认string避免浮点精度问题p.arrayT(toJsValue?, toDbValue?)可自定义数组元素的转换函数默认按字符串处理p.datetime(length?)、p.time(length?)可传入列长度参数p.jsonT()支持泛型标注 JSON 结构p.enum(items?)接受只读数组、字典或返回字典的函数p.embedded(target)、p.manyToOne(target)、p.oneToMany(target)、p.oneToOne(target)、p.manyToMany(target)分别对应五种属性类别关系类会在底层选项上设置kindm:1、1:m、1:1、m:n与延迟求值的entity: () target。几乎所有属性链方法都是“kind 受限”的例如.mappedBy()只对1:m/1:1/m:n可用.inversedBy()只对m:1/1:1/m:n可用.owner()只对1:1/m:n可用.fixedOrder()/.pivotTable()/.pivotEntity()仅对m:n可用。错误使用会在编译期返回never类型从而直接报错defineEntity.ts这是 defineEntity 类型安全的重要保障。常用链式方法速查方法作用适用属性.primary()标记主键标量.autoincrement()自增主键SQL标量.serializedPrimaryKey()序列化主键MongoDB 字符串id标量.nullable()/.strictNullable()允许空值 / 严格空值标量.unique(name?)/.index(name?)唯一约束 / 索引传字符串名可启用FindOptions.using类型安全标量SQL.default(v)/.defaultRaw(sql)默认值 / SQL 函数默认值标量SQL.onCreate(cb)/.onUpdate(cb)创建/更新时自动赋值标量.hidden()序列化时隐藏标量.version()乐观锁版本字段标量.columnType(t)/.length(n)/.precision(p)/.scale(s)数据库列类型控制Schema Generator标量SQL.formula(f)/.generated(g)/.check(c)公式列 / 生成列 / 检查约束标量SQL.eager()/.lazy()预加载 / 懒加载关系关系.cascade(...)/.orphanRemoval()级联 / 孤儿删除关系.mapToPk()外键映射为主键标量m:1/1:1.ref()/.lazyRef()类型安全引用见下节m:1/1:1.fixedOrder()/.pivotTable(t)固定顺序 / 自定义中间表m:n.orderBy(...)/.where(...)关系默认排序 / 过滤关系.joinColumn(c)/.deleteRule(r)/.updateRule(r)外键列名与级联规则关系关系修饰符.ref()与.lazyRef()对于m:1/1:1关系可以启用编译期的加载状态populate-state安全.ref()将运行时值包装为Reference暴露.$/.get()/.load()等 API——对应文档 type-safe-relations.md 中的RefT.lazyRef()仅类型层面的标记——运行时仍是普通实体无包装但 TypeScript 在Loaded收窄之前会隐藏非主键访问——对应 type-safe-relations.md 中的LazyRefT。const BookSchema defineEntity({ name: Book, properties: { id: p.integer().primary(), author: () p.manyToOne(AuthorSchema).ref(), // RefAuthor publisher: () p.manyToOne(PublisherSchema).lazyRef(), // LazyRefPublisher }, });源码层面.ref()会在选项上设置ref: true而.lazyRef()设置lazyRef: true二者互斥——对已标记lazyRef的属性调用.ref()会得到never类型反之亦然.lazyRef()也与.mapToPk()不兼容defineEntity.ts 与 L201-L205。MongoDB 实体示例defineEntity对 MongoDB 同样适用典型的ObjectId主键 序列化字符串主键组合const BookTagSchema defineEntity({ name: BookTag, properties: { _id: p.type(ObjectId).primary(), id: p.string().serializedPrimaryKey(), name: p.string(), books: () p.manyToMany(Book).mappedBy(tags), }, }); export class BookTag extends BookTagSchema.class {} BookTagSchema.setClass(BookTag);纯defineEntity等价写法export const BookTag defineEntity({ name: BookTag, properties: { _id: p.type(ObjectId).primary(), id: p.string().serializedPrimaryKey(), name: p.string(), books: () p.manyToMany(Book).mappedBy(tags), }, }); export type IBookTag InferEntitytypeof BookTag;这里_id使用p.type(ObjectId)接入 MongoDB 的 ObjectId 类型并标记为主键id用.serializedPrimaryKey()声明为序列化主键对应查询返回中的字符串id。注册生命周期钩子Hooks钩子生命周期事件有两条注册途径hooks属性——直接在defineEntity调用中传入“事件名 → 处理函数数组”的对象addHook方法——实体定义完成后再注册。两种方式都接受普通函数、具名函数与 async 函数实体实例通过args.entity获取。完整钩子列表与EventArgs细节见 events.md。defineEntity class模式下的钩子推荐在类定义完成后使用addHook以获得完整类型安全const BookTagSchema defineEntity({ name: BookTag, properties: { _id: p.type(ObjectId).primary(), id: p.string().serializedPrimaryKey(), name: p.string(), version: p.integer(), books: () p.manyToMany(Book).mappedBy(tags), }, }); export class BookTag extends BookTagSchema.class {} BookTagSchema.setClass(BookTag); BookTagSchema.addHook(beforeCreate, (args: EventArgsBookTag) { args.entity.version 1; }); BookTagSchema.addHook(beforeUpdate, (args: EventArgsBookTag) { args.entity.version; });纯defineEntity模式下的钩子export const BookTag defineEntity({ name: BookTag, properties: { _id: p.type(ObjectId).primary(), id: p.string().serializedPrimaryKey(), name: p.string(), version: p.integer(), books: () p.manyToMany(Book).mappedBy(tags), }, }); export type IBookTag InferEntitytypeof BookTag; BookTag.addHook(beforeCreate, (args: EventArgsIBookTag) { args.entity.version 1; }); BookTag.addHook(beforeUpdate, (args: EventArgsIBookTag) { args.entity.version; });也可以把钩子以内联hooks属性传入但此时args.entity会被类型化为any——因为实体类型此时尚未确定显式标注参数类型如EventArgsIBookTag也不行会造成循环引用。因此文档明确建议在类和类型别名定义完成之后再调用addHook以获得完整类型安全。从源码看addHook会把处理函数 push 进this._meta.hooks[event]数组EntitySchema.ts与通过hooks属性传入的钩子合并后统一由事件系统调度。DefineEntityHooks接口声明了onInit、onLoad、beforeCreate、afterCreate、beforeUpdate、afterUpdate、beforeUpsert、afterUpsert、beforeDelete、afterDelete共十类事件defineEntity.ts。底层 APIEntitySchema低层defineEntity返回的就是EntitySchema实例——也就是你可以直接实例化的那个类。直接使用EntitySchema通常没有必要defineEntity已提供更符合人体工学的 API 与完整类型推断但它仍可用于高级场景或 vanilla JavaScript 项目export interface IBook { title: string; author: Author; publisher: Publisher; tags: CollectionBookTag; } export const BookSchema new EntitySchemaIBook({ name: Book, extends: CustomBaseEntitySchema, properties: { title: { type: string }, author: { kind: m:1, entity: () Author, inversedBy: books }, publisher: { kind: m:1, entity: () Publisher, inversedBy: books }, tags: { kind: m:n, entity: () BookTag, inversedBy: books, fixedOrder: true }, }, });注意低层 API 的属性写法是纯对象形式标量用{ type: string }关系用{ kind: m:1, entity: () ... }而不是p.string()之类的构建器。对比可见defineEntity的价值正是把这些手写对象换成类型安全的链式构建器。使用类配合EntitySchema也可以传入class选项代替nameexport class Author extends CustomBaseEntity { name: string; email: string; constructor(name: string, email: string) { super(); this.name name; this.email email; } } export const AuthorSchema new EntitySchema({ class: Author, extends: CustomBaseEntitySchema, properties: { name: { type: string }, email: { type: string, unique: true }, }, });配置项参考EntitySchema的参数要求name与class二选一。使用class时extends会自动推断。其余参数name: string; class: ConstructorT; extends: string; tableName: string; // alias for collection: string properties: { [K in keyof T string]: EntityPropertyT[K] }; indexes: { properties: string | string[]; name?: string; type?: string }[]; uniques: { properties: string | string[]; name?: string }[]; repository: () ConstructorEntityRepositoryT; hooks: PartialRecordkeyof typeof EventType, ((string keyof T) | NonNullableEventSubscriber[keyof EventSubscriber])[]; abstract: boolean; orderBy: QueryOrderMapT | QueryOrderMapT[]; // default ordering for the entity参数逐项说明name/class实体标识二者必填其一class模式下extends自动从原型链推断extends基类或基类 schema对应前面介绍的继承机制tableName数据库表名等价于collectionMongoDB 术语properties属性映射key为属性名、值为EntityProperty描述对象indexes/uniques表级索引与唯一约束properties支持字符串或字符串数组repository自定义EntityRepository工厂hooks事件钩子映射abstract是否抽象实体不产生独立表对应 mapped superclass 概念见 inheritance-mapping.mdorderBy实体默认排序支持单个或多个排序映射。作为type的值除了字符串类型名还可以直接使用String/Number/Boolean/Date构造函数。实战决策建议综合文档与源码给出如下选型建议新项目默认使用defineEntity class模式兼具类型安全、hover 可读性、性能与自定义方法能力极简实体可用纯defineEntity配合InferEntity提取类型但要注意 hover 类型复杂与动态生成类的性能折衷公共字段优先用属性对象组合需要构造函数级默认值new即可用用extends 属性初始化器需要领域方法暴露给子类型则extends指向基类 class自定义类型通过p.type()接入并可用$typeT()覆盖推断类型源码$type支持Runtime/Raw/Serialized三泛型defineEntity.ts钩子在类和类型别名定义完成后再用addHook注册避免any与循环引用vanilla JavaScript 或高级场景才需要直接使用低层EntitySchemaAPI。相关源码与测试的进一步阅读入口实现defineEntity定义与属性构建器位于 packages/core/src/entity/defineEntity.ts低层实现EntitySchema与addHook/setClass位于 packages/core/src/metadata/EntitySchema.ts测试覆盖组合、继承、全部属性类型与关系种类、钩子与索引选项的用例位于 tests/defineEntity.test.ts相邻文档define-entity.md7.2 版、custom-types.md、type-safe-relations.md、inheritance-mapping.md、events.md赞分享后端【免费下载链接】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 实体模式EntitySchema完全指南以编程方式定义实体与 defineEntity 类型推导MikroORM v6.6 实体模式EntitySchema完全指南以编程方式定义实体与 defineEntity 类型推导 本篇技术指南聚焦 Mikro后端Loop免费开源的 macOS 窗口管理器一个触发键搞定分屏、跨屏与收纳Loop免费开源的 macOS 窗口管理器一个触发键搞定分屏、跨屏与收纳 窗口乱了是每天的常态拖到精确的半屏位置要来回找快捷键组合要翻文档背外接屏一连后端使用 defineEntity 以编程方式定义 MikroORM 实体无装饰器实体建模完整指南使用 defineEntity 以编程方式定义 MikroORM 实体无装饰器实体建模完整指南 defineEntity 是 MikroORM 提供的、无需装后端上一篇TinyMCE移动端适配确保在手机和平板上完美运行下一篇5大实战场景深度解析Android内核级权限管理解决方案完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考