关系:两端与基数

第 1 课 · 共 6 课 约 8 分钟

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

本课目标

读完这一课,你将能够

  • 说出关系的两端,解释为什么不必再建反向关系
  • 分清一对一、一对多、多对多,说出各自怎么存
  • 写出带外键、带链接属性的关系定义

关系有两端,一条就够

上一门课把示例工厂的对象类型建好了,这一门给它们连线。关系(Link Type)是两个对象类型之间的关系定义;链接(link)是这个关系的一个实例,比如「客户甲有订单 SO-1001」这一条。把它想成数据库里的 join:关系是 join 的定义,链接是 join 出来的一行。

每个关系有且只有两端,每一端有自己的名字。名字要读起来像一句话:从客户出发叫 orders,从销售订单出发叫 customer。一条定义,两头都能走,所以不要再为反方向另建一条关系。取名的办法是问自己:站在这一头,人会怎么说?「这家客户的订单」就是 orders,「这张订单的客户」就是 customer。

从客户出发从销售订单出发
端的名字orderscustomer
走到几个多个订单一个客户
回答的问题这家客户有哪些订单这张订单属于谁

同一对对象类型之间可以有几个关系,前提是每个都是不同的业务事实。两条规则在定义时检查:同一个对象类型上,链接端的名字不能重复;链接端的名字也不能和这个类型自己的属性同名,因为链接和属性共用一个命名空间。所以订单上的外键属性叫 customerId,链接端叫 customer。

基数决定怎么存

基数说的是每一端最多对应几个对象,它同时决定这条关系存在哪里。

ONE_TO_ONE

一对一

外键放在一头,引用另一头的主键。一对一只表达意图,并不强制唯一。

ONE_TO_MANY

一对多

外键放在「多」的一头,指向「一」的主键:salesOrder.customerId 指向 customer.customerId。从另一头写就是 MANY_TO_ONE。

MANY_TO_MANY

多对多

两边都装不下一个外键。每一条链接单独存一条记录,记着两端的主键,还可以带自己的属性。

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 该写什么。

小测

选一个答案,马上看解析。

Q1salesOrder 到 customer 是多对一。外键该放在哪?

Q2每台产品用多少某种物料,该放在哪里?

Q3已有 customerOrders(客户到订单)。还需要再建一条「订单到客户」的关系吗?

延伸阅读