后端【免费下载链接】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点击查看免费下载本文以 MikroORMTypeScript 数据映射 ORM官方文档的 Composite Primary Keys 章节为骨架系统讲解复合主键Composite Primary Keys的定义、持久化、查询与关联映射语义既覆盖name year这类纯原始类型组合也深入以外键实体作为主键的派生身份derived identity模式并演示动态属性、用户地址、带元数据连接表三大实战用例最后结合仓库源码typings.ts、Utils.ts与测试composite-keys.sqlite.test.ts说明其底层实现。读完本文你将掌握在 MikroORM 中设计复合主键实体、用对象或元组形式查询、在ManyToOne/OneToOne上声明primary: true以及通过pivotEntity自定义多对多中间表实体的完整实战方案。MikroORM 对复合主键的原生支持自3.5 版本起提供当前仓库的 v6.6 及更新版本文档均已完整覆盖该特性见 docs/versioned_docs/version-6.6/composite-keys.md。复合主键概述与通用约定MikroORM 原生支持复合主键。复合键是关系数据库中非常强大的概念MikroORM 尽可能覆盖了复合主键的各种使用场景支持纯原始数据类型组成的复合主键如string number支持外键作为主键identity through foreign entities支持把复合主键实体用在关联中MikroORM 会为相关实体自动生成与主键列一一对应的外键列。本文接下来展示复合主键的语义以及它们如何映射到数据库层面。通用约定General Considerations主键的值必须在调用em.persist(entity)之前设置好。复合主键没有自增能力因此在持久化前就必须完整填充所有主键字段。一个优雅的做法是像下文示例那样把主键值作为构造函数必传参数。纯原始类型复合主键以「车型 年份」为例假设要建立一个汽车数据库用model-name车型名和year生产年份联合作为主键。MikroORM 提供多种实体定义方式下面四种写法语义完全等价。方式一reflect-metadata 装饰器Entity() export class Car { PrimaryKey() name: string; PrimaryKey() year: number; // 用于 FilterQuery 的正确类型检查 [PrimaryKeyProp]?: [name, year]; constructor(name: string, year: number) { this.name name; this.year year; } }方式二ts-morph 装饰器Entity() export class Car { PrimaryKey() name: string; PrimaryKey() year: number; // 用于 FilterQuery 的正确类型检查 [PrimaryKeyProp]?: [name, year]; constructor(name: string, year: number) { this.name name; this.year year; } }方式三defineEntity无类方案export const Car defineEntity({ name: Car, properties: p ({ name: p.string(), year: p.integer(), }), primaryKeys: [name, year], });方式四EntitySchema显式 Schemaexport interface ICar { name: string; year: number; [PrimaryKeyProp]?: [name, year]; // 用于 FilterQuery 的正确类型检查 } export const Car new EntitySchemaICar({ name: Car, properties: { name: { type: string, primary: true }, year: { type: number, primary: true }, }, });持久化与查询定义好实体后即可正常使用const car new Car(Audi A8, 2010); await em.persist(car).flush();查询时必须提供全部主键两种写法均可// 写法一对象形式的条件 const audi1 await em.findOneOrFail(Car, { name: Audi A8, year: 2010 }); // 写法二主键元组数组顺序与主键定义顺序一致 const audi2 await em.findOneOrFail(Car, [Audi A8, 2010]);若想使用第二种「主键元组」写法必须像上面的Car实体那样通过PrimaryKeyPropsymbol 声明实体主键的类型与顺序。当实体只有单个标量主键且属性名是id: number | string | bigint、_id: any或uuid: string之一时不需要声明PrimaryKeyProp。这个例子的另一个价值在于把name、year作为构造函数的必传参数就自然解决了em.persist()之前主键必须已有值的约束——对象在创建的那一刻主键就已经完整。PrimaryKeyProp 的底层作用PrimaryKeyProp是一个Symbol 常量其定义位于 packages/core/src/typings.ts#L188-L189/** Symbol used to declare the primary key property name(s) on an entity (e.g., [PrimaryKeyProp]?: id). */ export const PrimaryKeyProp Symbol(PrimaryKeyProp);在类型层面typings.ts#L366-L389 会依据[PrimaryKeyProp]的声明来解析实体的主键类型PrimaryPropertyT这正是em.findOneOrFail(Car, [Audi A8, 2010])这类元组查询能被 TypeScript 严格校验类型的原因。此外在 EntityHelper.ts#L195 中PrimaryKeyProp与OptionalProps、EagerProps等一起被列为实体上需要特殊处理剔除出普通属性序列化/赋值流程的元数据符号。复合主键实体参与关联复合主键实体同样可以用于关联。若其他实体通过ManyToOne引用CarMikroORM 会为引用方生成两个外键列——分别对应name和year。这一点可以在 QueryBuilder 章节生成的 SQL 中直观看到外键列形如car_name、car_year。以外键实体确定身份Identity through Foreign Entities大量业务场景下一个实体的身份由它的一个或多个父实体决定典型场景包括实体的动态属性Dynamic Attributes例如Article的每个属性主键由article_id与attribute_name组成派生身份对象例如Person的Address地址主键就是user_id单列、由外键派生并非复合主键但身份同样来自外键实体带元数据的连接表Pivot Table例如两篇文章之间带描述与评分的关联可以建模为实体。映射语义非常简洁只有两条规则只允许出现在ManyToOne或OneToOne关联上在装饰器/属性定义中使用primary: true。用例一动态属性Article 与 ArticleAttribute文章可以有任意多个自定义属性每个属性的身份由article attribute联合确定。定义如下四种方式等价reflect-metadata / ts-morph 装饰器Entity() export class Article { PrimaryKey() id!: number; Property() title!: string; OneToMany(() ArticleAttribute, attr attr.article, { cascade: Cascade.ALL }) attributes new CollectionArticleAttribute(this); } Entity() export class ArticleAttribute { ManyToOne(() Article, { primary: true }) article: Article; PrimaryKey() attribute: string; Property() value!: string; [PrimaryKeyProp]?: [article, attribute]; // 用于 FilterQuery 的正确类型检查 constructor(name: string, value: string, article: Article) { this.attribute name; this.value value; this.article article; } }defineEntityexport const Article defineEntity({ name: Article, properties: p ({ id: p.integer().primary().autoincrement(), title: p.string(), attributes: () p.oneToMany(ArticleAttribute).mappedBy(article).cascade(Cascade.ALL), }), }); export const ArticleAttribute defineEntity({ name: ArticleAttribute, properties: p ({ article: () p.manyToOne(Article).primary(), attribute: p.string().primary(), value: p.string(), }), primaryKeys: [article, attribute], });EntitySchemaexport interface IArticle { id: number; title: string; attributes: CollectionArticleAttribute; } export interface IArticleAttribute { article: Article; attribute: string; value: string; [PrimaryKeyProp]?: [article, attribute]; // 用于 FilterQuery 的正确类型检查 } export const Article new EntitySchemaIArticle({ name: Article, properties: { id: { type: number, primary: true }, title: { type: string }, attributes: { kind: 1:m, entity: () ArticleAttribute, mappedBy: attr attr.article, cascade: [Cascade.ALL] }, }, }); export const ArticleAttribute new EntitySchemaIArticleAttribute({ name: ArticleAttribute, properties: { article: { kind: m:1, entity: () Article, primary: true }, attribute: { type: string, primary: true }, value: { type: string }, }, });注意ArticleAttribute的ManyToOne(() Article, { primary: true })外键article同时是主键的一部分这正是身份由父实体决定的体现。用例二简单派生身份User 与 Address有时需要两个对象通过OneToOne关联且被依赖方的实体复用依赖方的主键经典场景是用户与地址reflect-metadata / ts-morph 装饰器Entity() export class User { PrimaryKey() id!: number; OneToOne(() Address, address address.user, { cascade: [Cascade.ALL], nullable: true }) address?: Address; // 虚拟属性反向侧用于查询该关联 } Entity() export class Address { OneToOne(() User, { primary: true }) user!: User; [PrimaryKeyProp]?: user; // 用于 FilterQuery 的正确类型检查 }defineEntityexport const User defineEntity({ name: User, properties: p ({ id: p.integer().primary().autoincrement(), address: () p.oneToOne(Address).inversedBy(user).cascade(Cascade.ALL), }), }); export const Address defineEntity({ name: Address, properties: p ({ user: () p.oneToOne(User).primary(), }), primaryKeys: [user], });EntitySchemaexport interface IUser { id: number; address?: Address; } export interface IAddress { user: User; [PrimaryKeyProp]?: user; // 用于 FilterQuery 的正确类型检查 } export const User new EntitySchemaIUser({ name: User, properties: { id: { type: number, primary: true }, address: { kind: 1:1, entity: () Address, inversedBy: user, cascade: [Cascade.ALL] }, }, }); export const Address new EntitySchemaIAddress({ name: Address, properties: { user: { kind: 1:1, entity: () User, primary: true }, }, });这里Address的主键就是外键user单列派生身份。注意此时PrimaryKeyProp的声明是字符串user而非元组。用例三带元数据的连接表Order / Product / OrderItem在经典订单-商品场景中OrderItem同时引用Order与Product并携带额外数据购买数量amount、成交价offeredPrice这就是带元数据的连接表reflect-metadata / ts-morph 装饰器Entity() export class Order { PrimaryKey() id!: number; ManyToOne(() Customer) customer: Customer; OneToMany(() OrderItem, item item.order) items new CollectionOrderItem(this); Property() paid false; Property() shipped false; Property() created new Date(); constructor(customer: Customer) { this.customer customer; } } Entity() export class Product { PrimaryKey() id!: number; Property() name!: string; Property() currentPrice!: number; } Entity() export class OrderItem { ManyToOne(() Order, { primary: true }) order: Order; ManyToOne(() Product, { primary: true }) product: Product; Property() amount 1; Property() offeredPrice: number; [PrimaryKeyProp]?: [order, product]; // 用于 FilterQuery 的正确类型检查 constructor(order: Order, product: Product, amount 1) { this.order order; this.product product; this.offeredPrice product.currentPrice; } }defineEntityexport const Order defineEntity({ name: Order, properties: p ({ id: p.integer().primary().autoincrement(), customer: () p.manyToOne(Customer), items: () p.oneToMany(OrderItem).mappedBy(order), paid: p.boolean().default(false), shipped: p.boolean().default(false), created: p.datetime().onCreate(() new Date()), }), }); export const Product defineEntity({ name: Product, properties: p ({ id: p.integer().primary().autoincrement(), name: p.string(), currentPrice: p.float(), }), }); export const OrderItem defineEntity({ name: OrderItem, properties: p ({ order: () p.manyToOne(Order).primary(), product: () p.manyToOne(Product).primary(), amount: p.integer().default(1), offeredPrice: p.float(), }), primaryKeys: [order, product], });EntitySchemaexport interface IOrder { id: number; customer: Customer; items: CollectionOrderItem; paid: boolean; shipped: boolean; created: Date; } export interface IProduct { id: number; name: string; currentPrice: number; } export interface IOrderItem { order: Order; product: Product; amount: number; offeredPrice: number; [PrimaryKeyProp]?: [order, product]; // 用于 FilterQuery 的正确类型检查 } export const Order new EntitySchemaIOrder({ name: Order, properties: { id: { type: number, primary: true }, customer: { kind: m:1, entity: () Customer }, items: { kind: 1:m, entity: () OrderItem, mappedBy: item item.order }, paid: { type: boolean, default: false }, shipped: { type: boolean, default: false }, created: { type: Date }, }, }); export const Product new EntitySchemaIProduct({ name: Product, properties: { id: { type: number, primary: true }, name: { type: string }, currentPrice: { type: number }, }, }); export const OrderItem new EntitySchemaIOrderItem({ name: OrderItem, properties: { order: { kind: m:1, entity: () Order, primary: true }, product: { kind: m:1, entity: () Product, primary: true }, amount: { type: number, default: 1 }, offeredPrice: { type: number }, }, });自定义 pivotEntityv5.1 起支持默认情况下M:N 多对多关系在底层使用一个自动生成的连接表实体来表示 pivot 表。从v5.1开始可以通过pivotEntity选项提供自己的实现。pivot 实体必须满足约束恰好包含两个ManyToOne属性第一个指向 M:N 关系的拥有方owning entity第二个指向目标方target entity。Entity() export class Order { ManyToMany({ entity: () Product, pivotEntity: () OrderItem }) products new CollectionProduct(this); }对于双向 M:N 关系只需在拥有方指定pivotEntity两侧仍需通过inversedBy或mappedBy建立关联Entity() export class Product { ManyToMany({ entity: () Order, mappedBy: o o.products }) orders new CollectionOrder(this); }若要向这种 M:N 集合添加新元素需要让所有非外键属性都具备数据库层面的默认值否则插入时因缺少部分列而失败Entity() export class OrderItem { ManyToOne({ primary: true }) order: Order; ManyToOne({ primary: true }) product: Product; Property({ default: 1 }) amount!: number; }另一种做法是直接操作 pivot 实体// 创建新条目 const item em.create(OrderItem, { order: 123, product: 321, amount: 999, }); await em.persist(item).flush(); // 或通过 delete 查询移除条目 await em.nativeDelete(OrderItem, { order: 123, product: 321 });也可以像上一节那样定义指向 pivot 实体的 1:m 属性用它来修改集合同时保留 M:N 属性以方便读取与过滤。这两种视角可以共存**1:m 用于写M:N 用于读**。使用 QueryBuilder 处理复合键内部实现上复合键被表示为元组tuple其中元素顺序与主键定义顺序一致。下面的CarOwner通过ManyToOne引用Car复合主键QueryBuilder 的三种条件写法会生成不同的 SQLconst qb1 em.createQueryBuilder(CarOwner); qb1.select(*).where({ car: { name: Audi A8, year: 2010 } }); console.log(qb1.getQuery()); // select e0.* from car_owner as e0 where e0.name ? and e0.year ? const qb2 em.createQueryBuilder(CarOwner); qb2.select(*).where({ car: [Audi A8, 2010] }); console.log(qb2.getQuery()); // select e0.* from car_owner as e0 where (e0.car_name, e0.car_year) (?, ?) const qb3 em.createQueryBuilder(CarOwner); qb3.select(*).where({ car: [[Audi A8, 2010]] }); console.log(qb3.getQuery()); // select e0.* from car_owner as e0 where (e0.car_name, e0.car_year) in ((?, ?))三种写法语义各异写法生成的 SQL 语义{ car: { name: ..., year: ... } }展开为两个独立条件AND连接{ car: [Audi A8, 2010] }复合键整体做元组等值比较(a, b) (?, ?){ car: [[Audi A8, 2010]] }复合键元组集合做IN判断元组语义同样适用于获取复合主键实体的引用referenceconst ref em.getReference(Car, [Audi A8, 2010]); console.log(ref instanceof Car); // true源码与测试佐证复合主键的底层处理主键值的提取与序列化在 packages/core/src/utils/Utils.ts#L562-L584 中getPrimaryKeyValues负责从实体或嵌套实体中提取主键值若值是实体对象则递归调用helper(entity).getPrimaryKey()取得其主键若值是普通对象则展平取值。而 Utils.ts 中的joinPrimaryKeys/splitPrimaryKeys则负责把复合主键拼接成字符串 key以PK_SEPARATOR分隔或反向拆分——这是身份映射Identity Map与关联表元组索引的基础。WrappedEntity 的主键访问packages/core/src/entity/WrappedEntity.ts#L224-L322 提供了getPrimaryKey()、getPrimaryKeys()、getPrimaryKeyProp()等运行时方法getPrimaryKeys()在复合主键场景下会遍历所有主键列并递归收集子实体主键返回元组数组这些方法被 EntityManager 的find/findOne/getReference与序列化器广泛调用。测试用例覆盖仓库的 tests/features/composite-keys 目录提供了跨数据库的完整测试矩阵包括 SQLitecomposite-keys.sqlite.test.ts、MySQLcomposite-keys.mysql.test.ts、Oraclecomposite-keys.oracle.test.ts以及 MSSQL 元组比较tuple-comparison.mssql.test.ts等。其中 composite-keys.sqlite.test.ts 直接验证了本文的核心用法复合主键实体如FooParam12主键为[bar, baz]既支持元组数组查询em.findOneOrFail(FooParam12, [param.bar.id, param.baz.id] as const)也支持对象条件查询em.findOneOrFail(FooParam12, { bar: param.bar.id, baz: param.baz.id })见该文件第 301、313 行附近派生身份实体Address12可以直接用父实体主键查询复合主键实体参与ManyToOne关联后支持 JOINED 加载策略与双向关系Car12↔User12自定义 pivot 实体custom-pivot-entity*.sqlite.test.ts系列验证了pivotEntity选项在单向、双向、复杂、自动发现等场景下的行为。小结MikroORM 的复合主键支持覆盖了关系建模的绝大多数诉求纯原始类型组合多个PrimaryKey()属性联合作为主键配合PrimaryKeyProp获得元组查询的类型安全外键派生身份在ManyToOne/OneToOne上声明primary: true让实体身份由父实体决定动态属性、1:1 派生、带元数据连接表连接表实体化从 v5.1 起可用pivotEntity自定义 M:N 中间表实体实现1:m 写、M:N 读的双视角操作查询与引用对象条件、主键元组、元组IN三种方式以及getReference的元组形式引用底层统一由 Utils.ts 与 WrappedEntity.ts 的元组化处理支撑。在设计新表结构时若某个业务实体天然依赖一个父实体 一个业务键或两个父实体才能唯一定位复合主键就是最贴合数据库语义的选择——它省去了多余的代理主键列也让 MikroORM 的身份映射Identity Map与工作单元Unit of Work直接以业务键为单位工作。该部分文档的语义与行为参考了 Doctrine 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 复合主键Composite Primary Keys实战指南原始类型复合键、外键派生身份与带元数据联结表MikroORM 复合主键Composite Primary Keys实战指南原始类型复合键、外键派生身份与带元数据联结表 复合主键是关系型数据库中一个非后端MikroORM 复合主键与外键派生主键实战指南MikroORM 复合主键与外键派生主键实战指南 MikroORM 从 3.5 版本起原生支持复合主键Composite Primary Keys既能用多后端Sway 内建类型完全指南从原始类型到复合类型的智能合约数据体系Sway 内建类型完全指南从原始类型到复合类型的智能合约数据体系 本篇技术指南以 Sway 语言官方文档《Built in Types》为核心系统讲解 Sw编程语言编译器区块链上一篇终极指南如何使用Catalyst实现多交易所集成交易下一篇终极GalTransl指南三步零门槛制作高质量Galgame汉化补丁创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考