关系:两端与基数
两端各有名字;一对多用外键,多对多每条链接单独存,还能带属性;物料清单是典型例子。

本课目标
读完这一课,你将能够
- 说出关系的两端,解释为什么不必再建反向关系
- 分清一对一、一对多、多对多,说出各自怎么存
- 写出带外键、带链接属性的关系定义
关系有两端,一条就够
上一门课把示例工厂的对象类型建好了,这一门给它们连线。关系(Link Type)是两个对象类型之间的关系定义;链接(link)是这个关系的一个实例,比如「客户甲有订单 SO-1001」这一条。把它想成数据库里的 join:关系是 join 的定义,链接是 join 出来的一行。
每个关系有且只有两端,每一端有自己的名字。名字要读起来像一句话:从客户出发叫 orders,从销售订单出发叫 customer。一条定义,两头都能走,所以不要再为反方向另建一条关系。取名的办法是问自己:站在这一头,人会怎么说?「这家客户的订单」就是 orders,「这张订单的客户」就是 customer。
| 从客户出发 | 从销售订单出发 | |
|---|---|---|
| 端的名字 | orders | customer |
| 走到几个 | 多个订单 | 一个客户 |
| 回答的问题 | 这家客户有哪些订单 | 这张订单属于谁 |
同一对对象类型之间可以有几个关系,前提是每个都是不同的业务事实。两条规则在定义时检查:同一个对象类型上,链接端的名字不能重复;链接端的名字也不能和这个类型自己的属性同名,因为链接和属性共用一个命名空间。所以订单上的外键属性叫 customerId,链接端叫 customer。
基数决定怎么存
基数说的是每一端最多对应几个对象,它同时决定这条关系存在哪里。
一对一
外键放在一头,引用另一头的主键。一对一只表达意图,并不强制唯一。
一对多
外键放在「多」的一头,指向「一」的主键:salesOrder.customerId 指向 customer.customerId。从另一头写就是 MANY_TO_ONE。
多对多
两边都装不下一个外键。每一条链接单独存一条记录,记着两端的主键,还可以带自己的属性。
foreignKey.side 写的是哪一头持有外键属性:客户到订单,订单持有,所以是 to;订单行到产品,订单行持有,所以是 from。外键属性必须真的存在,类型要和被引用的主键一致;找不到它,整批定义会被拒收。
一个常见的坑:把一对一当成「唯一约束」。它只表达意图,两个对象的外键指向同一个对象,也不会被拦下。真要保证唯一,就在写入的那个 Action 里加一条提交条件。
[
{
"kind": "linkType",
"apiName": "customerOrders",
"title": "Orders of a customer",
"schema": {
"from": "customer",
"to": "salesOrder",
"cardinality": "ONE_TO_MANY",
"apiNameAtoB": "orders",
"apiNameBtoA": "customer",
"foreignKey": { "side": "to", "property": "customerId" }
}
},
{
"kind": "linkType",
"apiName": "orderLineProduct",
"title": "Product of an order line",
"schema": {
"from": "orderLine",
"to": "product",
"cardinality": "MANY_TO_ONE",
"apiNameAtoB": "product",
"apiNameBtoA": "orderLines",
"foreignKey": { "side": "from", "property": "sku" }
}
}
]
多对多:链接自己也有属性
物料清单(BOM)是最典型的多对多:一个产品用多种物料,一种物料也用在多个产品里。「每台用多少」既不属于产品,也不属于物料,它属于这一对产品和物料。所以它是链接上的属性 quantityPerUnit。
{
"kind": "linkType",
"apiName": "productMaterials",
"title": "Bill of materials",
"schema": {
"from": "product",
"to": "material",
"cardinality": "MANY_TO_MANY",
"apiNameAtoB": "materials",
"apiNameBtoA": "products",
"columns": [
{ "name": "quantityPerUnit", "type": "decimal", "title": "Quantity per unit" }
]
}
}
- 链接属性写在
columns里,类型照属性的 base type,最多 16 个。 - 一对产品和物料只有一条链接,由两端的主键定位。
- 把
quantityPerUnit放到material上是常见的错:同一种物料在不同产品里用量不同。
改关系也只走 Action。多对多用 createLink 和 deleteLink;外键关系改外键属性就行,给订单换客户,就是用 modifyObject 改 customerId。普通规则只改语义层。若 Action 配置了 webhooks.writeback,它会先调用 ERP,再执行规则。ERP 拒绝或超时,语义层不改。
下一节沿着这些关系走:从一个订单走到它的订单行,一批一批地换类型,最后读出 BOM 上的用量。
要点
- 一个关系有两端、各有名字,一条定义两头都能走,不用另建反向关系。
- 基数决定存法:一对一、一对多用外键,外键放在「多」的一头;多对多每条链接单独存。
- 只属于「这一对」对象的值,是链接上的属性,例如 BOM 里的
quantityPerUnit。 - 改关系走 Action:多对多用
createLink和deleteLink,外键关系改外键属性。
练一练
给示例工厂再连几条线
在自己的 ontology/ 目录里做,或者先写在纸上。
为 lineMachines(productionLine.machines / machine.line)和 supplierPurchaseOrders(supplier.purchaseOrders / purchaseOrder.supplier)各写一份 linkType,指出外键在哪个属性上、side 该写什么。
判断并写出基数和存法:「一张检验单有多个缺陷」「一台设备只在一条产线上」「一家供应商供多种物料,一种物料有多家供应商」。最后一个关系上该带什么属性?
把定义放进目录,运行 aidc semantic define ontology/ --dry-run,看引用是否闭合;再故意把 foreignKey.property 写错,读一读报错。
小测
选一个答案,马上看解析。
Q1salesOrder 到 customer 是多对一。外键该放在哪?
订单到客户是多对一;客户到订单是一对多。外键都放在订单这一头,指向客户的主键。联结表用于多对多。
Q2每台产品用多少某种物料,该放在哪里?
用量取决于「这一对」产品和物料:同一种物料在不同产品里用量不同,放在任何一个对象上都会含糊。
Q3已有 customerOrders(客户到订单)。还需要再建一条「订单到客户」的关系吗?
一个关系天生有两端,各有名字,任何一头都能走。再建一条只会得到重复的事实。