拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MikroORM 5 版本深度解读:更严格、更安全、更智能的 TypeScript ORM

MikroORM 5 版本深度解读:更严格、更安全、更智能的 TypeScript ORM 后端【免费下载链接】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 5 正式发布版2022 年 2 月为背景系统讲解该大版本在类型安全、实体刷新、Schema 差异对比Schema Diffing、自动 flush 模式、多态 embeddable、可 await 的 QueryBuilder 等核心方向上的架构性改进。文章面向正在使用或计划升级到 v5 的开发者阅读后可完整掌握 v5 的新配置项、新 API 用法及其底层实现机制并能结合当前仓库源码packages/core、packages/migrations、tests等理解每个特性背后的设计意图与调用链。MikroORM 5 的主题可以概括为三个词Stricter更严格的类型、Safer更安全的持久化、Smarter更智能的 Schema 与查询。v5 的开发工作始于 2021 年 3 月历时近一年才正式发布是继 4.x 系列之后的一次大规模重构。4.x 阶段回顾在深入 v5 之前先快速回顾 4.x 版本带来的重要能力这些能力构成了 v5 演进的基础结果缓存Result Cache允许缓存查询结果相关配置见 docs/docs/caching.md。自动事务上下文Automatic Transaction Context通过TransactionContext让事务中的查询自动共享同一个上下文连接。嵌套 embeddables支持在 embeddable 内部继续嵌套 embeddable并带来该领域的一系列改进详见 docs/docs/embeddables.md。使用环境变量配置 ORM允许通过环境变量驱动配置项详见 docs/docs/configuration.md 中的 Using Environment Variables 一节。这些特性为 v5 的「严格类型」与「智能持久化」两大主线打下了基础。全面改进的类型安全v5 最引人注目的特性是「几乎处处严格类型」em.create()、toJSON()、toObject()、populate、partial loading 以及 order by 提示全部获得了严格类型支持。下面是一个完整的示例先通过em.create()一次性构建整张实体图再通过严格类型的FindOptions加载实体并 populate 关联const god em.create(Author, { name: God, // 校验必填属性 email: godheaven.io, books: [{ title: Bible, part 1, tags: [{ name: old }, { name: bestseller }], }], }, { persist: true }); // 也可以通过全局配置 persistOnCreate: true 开启 await em.flush(); // 模拟新的请求 em.clear(); // authors 的类型是 LoadedAuthor, books.tags[] const authors await em.find(Author, {}, { populate: [books.tags], // 若未显式提供populate 提示可从 fields 自动推断 fields: [books.tags.name], // 支持点号记法的严格 partial loading orderBy: { name: asc, books: { tags: { name: asc } } }, // 支持对象嵌套的严格 order by }); // books 和 tags 会被类型为 LoadedCollection因此可以使用安全的 $ 访问器 console.log(authors[0].books.$[0].tags.$[0].name); const dto wrap(authors[0]).toObject(); console.log(dto.books[0].tags[0].name); // DTO 同样是严格类型的OptionalProps区分「类型必填」与「创建时可省」的属性em.create()会同时校验载荷的类型与可选性。问题在于实体上某些属性可能由钩子函数或数据库函数提供默认值——我们希望在类型层面将其定义为必填但在em.create()的上下文中它们应当被视为可选。v5 通过OptionalPropssymbol 解决这一问题Entity() export class Author { // 只有 name 会被 em.create() 视为必填 [OptionalProps]?: createdAt | updatedAt; PrimaryKey() id!: number; Property({ defaultRaw: current_timestamp() }) createdAt!: Date; Property({ onUpdate: () new Date(), length: 3, defaultRaw: current_timestamp(3) }) updatedAt!: Date; Property() name!: string; }一些属性名永远被视为可选id、_id、uuid。从当前仓库源码看em.create()的严格类型校验实现于 packages/core/src/EntityManager.ts其文档注释明确说明「参数会被严格检查必须提供所有必填属性可通过OptionalPropssymbol 将某些属性从检查中豁免也可使用partial: true选项关闭必填属性的严格检查该选项对运行时无影响」。严格类型的 EntityDTO实体的序列化形式可能非常不可预测——已加载关联 vs 引用、属性序列化器、惰性属性、自定义实体序列化器或toJSON方法、急加载、递归检查等都会影响最终输出。因此 v5 将EntityDTO类型上的所有关联都视为已加载这主要是为了获得更好的开发体验DX如果所有关联都被类型为PrimaryT | EntityDTOT例如number | EntityDTOBook就无法享受智能提示了const book {} as Book; const dto wrap(book).toObject(); // EntityDTOBook // 现在这可以直接访问而使用 PK 联合类型时则需要处处类型断言 const name dto.author.name;在某些场景下DTO 仍可能需要显式类型转换这是类型系统为「可预测的 DX」所做的合理取舍。运行时校验与 CLI 一致性检查在编译期类型校验之上v5 还新增了运行时校验在 insert 查询真正发出之前会先确保必填属性都有值。这对 MongoDB 尤其重要因为 Mongo 没有 schema 层面的可选性检查。此外 v5 还增加了两项使用体检能力CLI 本地安装警告如果直接使用未在本地安装的 CLI会收到警告提示。ORM 包一致性校验如果忘记升级部分 ORM 包导致版本不匹配、甚至安装了多个 core 包v5 也会主动校验并给出提示避免「幽灵问题」。彻底重写的 Schema DiffingSchema diffing 曾经是 MikroORM 最薄弱的环节之一经常产生多余的查询甚至无法达到完全同步的状态。v5 对 Schema diffing 进行了彻底重写修复了所有已知问题并额外加入了一批能力外键约束foreign key constraints的 diffing正确的索引 diffing此前只比较名称自定义索引表达式custom index expressions注释commentdiffing列长度 diffing例如numeric(10,2)或varchar(100)主键类型变更Schema/命名空间 diffing仅 Postgres自动生成 down migrations暂不支持 SQLiteCheck 约束支持仅 Postgres这意味着基于 v5 的schema:update、schema:diff与迁移生成流程能给出更精确的差异结果显著减少了「手动补齐」的工作量。更智能的迁移Migrations在生产环境我们通常希望使用编译后的迁移文件。从 v5 起这几乎可以开箱即用——只需要正确配置迁移路径。已执行的迁移现在会忽略文件扩展名因此可以在同一个数据库上混用 node 与 ts-node 执行迁移且保持向后兼容import { MikroORM, Utils } from mikro-orm/core; await MikroORM.init({ migrations: { path: dist/migrations, pathTs: src/migrations, }, // 或者根据运行环境动态选择 // migrations: { // path: Utils.detectTsNode() ? src/migrations : dist/migrations, // }, // ... });另一个重要变化是创建新迁移时会自动把目标 Schema 快照保存到迁移文件夹。之后创建新迁移时会使用该快照、而不是当前数据库 Schema 作为比对基准。这意味着即使在运行挂起迁移之前就尝试创建新迁移依然能得到正确的 Schema 差异若没有额外变更也不会生成空迁移。快照应该像普通迁移文件一样纳入版本控制。从当前仓库源码看这一机制至今仍在演进packages/migrations/src/Migrator.ts 中快照路径会根据emit选项ts与pathTs否则path确定目录并生成形如.snapshot-dbName.json的快照文件名称可通过snapshotName配置覆盖。Auto-flush 模式再也不丢失内存中的变更此前 flush 永远是一个显式动作。v5 引入了可配置的 flush 策略工作方式与 JPA/Hibernate 类似共有三种模式模式行为FlushMode.COMMITEntityManager延迟 flush直到当前事务提交FlushMode.AUTO默认模式仅在必要时 flushEntityManagerFlushMode.ALWAYS在每次查询前都 flushEntityManagerFlushMode.AUTO会尝试检测正在查询的实体是否存在重叠变更若有则触发 flush// 查询 author 时若存在新 persist 的 author会触发 auto-flush const a1 new Author(...); em.persist(a1); const r1 await em.find(Author, {}); // 查询 author 时若只有新 book 而没有 author 变更不会触发 auto-flush const b4 new Book(...); em.persist(b4); const r2 await em.find(Author, {}); // 但查询 book 时会触发 auto-flush const r3 await em.find(Book, {});关于 flush 模式的完整文档见 docs/docs/unit-of-work.md 的 Flush Modes 一节。从源码看FlushMode.AUTO是全局默认值见 packages/core/src/utils/Configuration.ts 中的flushMode: FlushMode.AUTO。查询前的 flush 决策发生在 packages/core/src/EntityManager.tsCOMMIT模式直接返回、ALWAYS模式无条件 flush而AUTO模式则委托给UnitOfWork.shouldAutoFlush(meta)。shouldAutoFlush的实现位于 packages/core/src/unit-of-work/UnitOfWork.ts它首先检查是否正处于 flush 过程中避免递归随后检查被查询实体的类或其根类是否有排队动作queued actions并额外处理了单表继承场景下的discriminatorMap——即子类有变更时也会触发对父类查询的 flush。这也解释了上面示例中的行为Author有变更时查询Author会 flush而只有Book变更时查询Author不会 flush因为两者不重叠。已加载实体的自动刷新此前当一个实体已经加载、又需要重新加载时必须在选项中显式传入refresh: true。刷新还有一个副作用用于计算 changeset 的实体数据总是基于新加载的实体更新因而会忘记之前的状态——这可能导致刷新前的内存修改被静默丢失。v5 改变了这一行为新加载的数据总是与当前状态合并当发现某个属性已被更新时保留被修改的值。此外对于带主键条件的em.findOne()v5 会通过比较选项与已加载属性名来推断「是否有必要重新加载」并在此过程中考虑fields与populate选项以同时支持 partial loading 与惰性属性// 先只加载 author 的 id 与 email const a1 await em.findOneOrFail(Author, 123, { fields: [id, email] }); a1.email lol; // 修改 email // 用相同字段重新加载不会发查询和之前一样 const a2 await em.findOneOrFail(Author, 123, { fields: [email] }); console.log(a1 a2); // true同一实体实例未发查询 // 用额外字段重新加载无需 refresh: true const a3 await em.findOneOrFail(Author, 123, { fields: [id, age] }); console.log(a1 a3); // true同一实体实例但已更新 console.log(a1.age); // 新值已加载 a1.age 1000; // 覆盖为新值 // 加载完整实体同样无需 refresh: true const a4 await em.findOneOrFail(Author, 123, { populate: [books] }); console.log(a1 a4); // true同一实体实例但已更新 console.log(a1.termsAccepted); // 新值已加载 await em.flush(); // 用新的 email 与 age 更新 author对于em.findOne()的复杂条件以及em.find()的一切查询v5 仍然总是执行查询但不再像以前那样忽略已加载实体的数据而是以同样的方式合并// 先只加载 author 的部分字段 const r1 await em.find(Author, {}, { fields: [id] }); r1[0].email lol; // 修改其中一个 email console.log(r1[0].name); // undefined未加载 // 重新加载完整实体 —— 无需 refresh: true const r2 await em.find(Author, {}); console.log(r2[0]); // 完整加载的 author但 email 已被改为 lol console.log(r1[0] r2[0]); // true同一实例只是被更新 // flush 现在只会发一条 update 查询修改其中一个 author 的 email await em.flush();这一「合并而非覆盖」的策略正是 v5 强调 Safer 的核心体现之一在 Identity Map 语义下已加载实体的内存状态不再轻易被数据库回读覆盖。Seeder 包数据库播种MikroORM v5 新增了独立的Seeder包mikro-orm/seeder用于向数据库写入初始数据或测试数据。它复用与常规操作一致的EntityManagerAPI并在此基础上增加了实体工厂entity factories能力可通过 faker社区新发布的版本生成假数据。完整的示例与用法参见 docs/docs/seeding.md。仓库中也有对应的测试用例见 tests/database/seeder 目录。多态 Embeddables多态 embeddables 允许为单个 embedded 属性定义多个类运行时根据判别列discriminator column选择正确的类工作机制类似单表继承。v5 中该能力仅对 embeddables 生效对多态实体的支持计划在后续 5.x 版本中加入Entity() class Owner { PrimaryKey() id!: number; Property() name!: string; Embedded(() [Cat, Dog]) pet!: Cat | Dog; }完整的示例见 docs/docs/embeddables.md 的 Polymorphic Embeddables 一节。embeddables 领域还有一系列小改进与问题修复例如支持 many-to-one 关联只存储主键并像普通实体一样支持 populate 该关联。支持onCreate与onUpdate属性选项。Populate 惰性标量属性此前填充populate一个惰性标量属性的唯一方式是在包含它的实体首次加载时完成。如果该实体已存在于 Identity Map 中且未包含该属性就必须刷新其状态——这又可能丢失部分内存状态。MikroORM v5 允许通过em.populate()填充这类属性且绝不会覆盖实体上已有的内存变更。无需 EntityManager 创建引用过去要创建一个「仅由主键表示的实体引用」必须拿到当前的EntityManager实例因为这类实体总是需要被管理。v5 在Reference类上新增了辅助方法可以在没有EntityManager的情况下创建实体引用——例如在实体构造函数内部Entity() export class Book { ManyToOne(() Author, { wrappedReference: true }) author!: IdentifiedReferenceAuthor; constructor(authorId: number) { this.author Reference.createFromPK(Author, authorId); } }Reference包装类是可选的用于对关联提供更多类型安全。也可以使用Reference.createNakedFromPK()。这样创建的引用是未托管unmanaged的会在宿主实体被 flush 时合并到EntityManager。注意在 flush 之前Reference.init()或Reference.load()等方法不可用因为它们依赖EntityManager实例。从当前仓库源码看这两个方法至今保留在 packages/core/src/entity/Reference.tscreateFromPK()内部先调用createNakedFromPK()生成「裸」实体再通过helper(ref)?.toReference()包装为Reference而createNakedFromPK()通过实体原型上的__factoryEntityFactory调用factory.createReference(entityType, pk, { merge: false, convertCustomTypes: false })创建未托管的引用并把主键属性标记为已加载、预置原始实体数据。当前版本还提供了便捷的ref()与rel()辅助函数分别是两个 create 方法的简写。仓库测试中也大量使用了该 API例如 tests/EntityManager.oracledb2.test.ts 验证了「未托管引用在 flush 后变为托管、或被同实体的已托管引用替换」的行为tests/features/deferrable-constraints/deferrable-constraints.postgres.test.ts 则用它构造父子引用以测试延迟约束。更智能的expr辅助函数expr()辅助函数用于绕过严格类型限制。它本质上是一个恒等函数唯一作用是告诉 TypeScript「这个值实际上是另一种类型」确切地说是泛型字符串。v5 为它增加了两种新用法回调签名支持表达式的动态别名alias。数组参数支持元组比较。import { expr } from mikro-orm/core; const res1 await em.find(Book, { // 类型参数可选传入可获得实体属性自动补全 [exprBook([price, createdAt])]: { $lte: [100, new Date()] }, }); // 会生成类似查询 // select b0.* from book as b0 where (b0.price, b0.created_at) (?, ?) const res2 await em.find(Book, { // 类型参数可选传入可获得实体属性自动补全 [expr(as lower(${as}.name))]: jon, }); // 会生成类似查询 // select b0.* from book as b0 where lower(b0.name) ?演进说明expr()在 v6 中被移除取而代之的是raw()静态辅助函数与sql标签模板函数详见 docs/blog/2024-01-08-mikro-orm-6-released.md因此 v5 中「可选」的标记在 v6 起成为必需。可 await 的 QueryBuilderQueryBuilder 现在感知自身类型getResult()与execute()方法会根据类型给出对应返回类型而且可以直接 await QueryBuilder 实例——await 会自动执行 QB 并返回合适的结果。QB 实例会根据使用的select/insert/update/delete/truncate方法被类型化为以下六种之一QB 类型await 结果SelectQueryBuilder实体数组CountQueryBuildernumberInsertQueryBuilderQueryResultUpdateQueryBuilderQueryResultDeleteQueryBuilderQueryResultTruncateQueryBuilderQueryResultconst res1 await em.createQueryBuilder(Publisher).insert({ name: p1, type: PublisherType.GLOBAL, }); // res1 类型为 QueryResultPublisher console.log(res1.insertId); const res2 await em.createQueryBuilder(Publisher) .select(*) .where({ name: p1 }) .limit(5); // res2 类型为 Publisher[] console.log(res2.map(p p.name)); const res3 await em.createQueryBuilder(Publisher).count().where({ name: p1 }); // res3 类型为 number console.log(res3 0); const res4 await em.createQueryBuilder(Publisher) .update({ type: PublisherType.LOCAL }) .where({ name: p1 }); // res4 类型为 QueryResultPublisher console.log(res4.affectedRows 0); const res5 await em.createQueryBuilder(Publisher).delete().where({ name: p1 }); // res5 类型为 QueryResultPublisher console.log(res5.affectedRows 0); expect(res5.affectedRows 0).toBe(true); // 测试类型这一设计让「链式构建 直接执行」成为类型安全的惯用法相关能力持续演进出em.findByCursor()等后续 API见 packages/core/src/EntityManager.ts 的游标分页实现。通配符 Schema 实体此前我们可以定义「特定 schema 中的实体」或「无 schema 的实体」后者会基于 ORM 配置或FindOptions使用 schema。这允许从特定 schema 读取实体但失去了 Unit of Work 的能力。v5 中实体实例现在持有 schema 名称作为WrappedEntity的一部分。被托管的实体将从FindOptions或元数据中取得 schema创建新实体实例的方法如em.create()、em.getReference()新增了 options 参数以设置 schema同时可用wrap(entity).getSchema()与wrap(entity).setSchema()读写。实体可通过Entity({ schema: * })声明通配符 schema其规则如下指定 schema实体只存在于该 schema定义*schema实体可存在于任意 schema始终由参数控制省略 schema 选项取值来自全局 ORM 配置。通配符 schema 实体在未指定 schema 选项时会被SchemaGenerator忽略。更完整的讨论见 docs/docs/multiple-schemas.md 的 Wildcard Schema 一节。实体的深度赋值Deep Assigningwrap().assign()原本设计为更新单个实体及其值但大量用户希望一次赋值整张实体图、同时更新关联。v5 改变了EntityAssigner对「应更新哪个实体」的检测方式深度实体图赋值默认可用、无需额外选项。它基于实体主键匹配工作——如果你想对某个关联发出更新而不是新建关联请先加载该关联并把其主键传给 assign 辅助函数const book await em.findOneOrFail(Book, 1, { populate: [author] }); // 更新已有 book 的 author 名称 wrap(book).assign({ author: { id: book.author.id, name: New name..., }, });如果希望即使数据中不包含实体主键也总是更新实体可以使用updateByPrimaryKey: falseconst book await em.findOneOrFail(Book, 1, { populate: [author] }); // 更新已有 book 的 author 名称 wrap(book).assign({ author: { name: New name..., }, }, { updateByPrimaryKey: false });更多示例见 docs/docs/entity-helper.md 的 Updating Deep Entity Graph 一节。对 ES Modules 的实验性支持虽然 MikroORM v5 仍以 CommonJS 编译和发布但加入了若干改进以支持在 ESM 项目中使用使用gen-esm-wrapper包支持具名导入named imports通过一个技巧保持动态 import而不编译为 require 语句——为此需要设置MIKRO_ORM_DYNAMIC_IMPORTS环境变量。这使基于文件夹的实体发现folder-based discovery在 ES Modules 下成为可能而此前这是不可行的。其他值得注意的变化Partial loadingfields支持 joined 加载策略。AsyncLocalStorage成为RequestContext辅助函数的默认实现。新增onLoad事件类似onInit但允许异步且只对已加载实体触发、不触发引用。CLI 配置支持导出 async 函数。SQL 的可配置别名策略aliasing strategy。允许提供自定义 Logger 实例详见 docs/docs/logging.md。em.create()的persist选项与全局persistOnCreate配置见 docs/docs/configuration.md 的 Persist Created Entities Automatically 一节当前源码中该选项的解析逻辑见 packages/core/src/EntityManager.tsoptions.persist ?? em.config.get(persistOnCreate)。实体生成器Entity Generator支持 M:N 关系。支持指定事务隔离级别。可控制 populate 提示的 where 条件见 docs/docs/loading-strategies.md 的 Population Where Condition 一节。API 文档全面改版。更多改动可查阅 packages/core/CHANGELOG.md。从 4.x 升级到 5.x 的完整注意事项见 docs/docs/upgrading-v4-to-v5.md。v5 之后的演进方向发布文档中还预告了后续版本的重点方向其中多项已在后续版本落地为 M:N 关系指定pivot 实体可在其中增加额外列同时仍按 M:N 读取——v5.x 中已实现支持数据库视图或表示 SQL 表达式的实体——后续版本以 View Entity 形式实现见 docs/docs/view-entities.md更多驱动支持better-sqlite3、CockroachDB——已在后续版本中陆续加入。从当前仓库的结构看v5 奠定的这些基础能力严格类型、Auto-flush、Schema 快照迁移、引用工厂、可 await 的 QueryBuilder、通配符 schema、深度赋值大多延续至今并持续增强构成 MikroORM 后续大版本v6、v7的底层骨架。小结MikroORM 5 是一次「正确性优先」的大版本升级类型系统从「宽松可用」走向「严格可推断」持久化语义从「显式 flush 覆盖式刷新」走向「自动 flush 合并式刷新」Schema 工具链从「粗粒度 diff」走向「精确到列、索引、约束与快照」的完整闭环。对于追求类型安全与运行时一致性的 TypeScript/Node.js 项目v5 既是生产可用的稳定版本也是理解 MikroORM 现代架构的最佳切入点。赞分享后端【免费下载链接】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点击查看免费下载相关推荐PhpSpreadsheet 变更日志深度解读从 1.0 到 5.10 的版本演进、破坏性变更与安全实践PhpSpreadsheet 变更日志深度解读从 1.0 到 5.10 的版本演进、破坏性变更与安全实践 本文以仓库根目录的 CHANGELOG.md htt后端ClickHouse v25.1.1.4165-stable 版本变更深度解读新特性、不兼容变更与性能优化全览ClickHouse v25.1.1.4165 stable 版本变更深度解读新特性、不兼容变更与性能优化全览 本文基于仓库中的 v25.1.1.4165 s数据库OLAP列式数据库大数据实时分析数据分析Diesel ORM 版本演进全解析从 2.3/2.4 新特性到 1.x 历史变更深度导读Diesel ORM 版本演进全解析从 2.3/2.4 新特性到 1.x 历史变更深度导读 Diesel 是 Rust 生态中主打“安全、可扩展”的 ORM后端数据库上一篇10分钟跑通DBCHM数据库字典导出完整上手指南下一篇把 SVG 变成可拖拽拓扑图只需几步vue-webtopo-svgeditor 完整上手指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表