沿着关系走

第 2 课 · 共 6 课 约 8 分钟

从一个对象走一步,用 pivotTo 一批一批地走,用派生属性把关系数成数字,在数据库里读链接上的属性。

本课目标

读完这一课,你将能够

  • 沿一条关系取到相关的对象,分清返回一个还是一批
  • 用 pivotTo(searchAround)把一个对象集换成它的相关对象集
  • 沿关系数出数字,并读出多对多关系上的属性

从一个对象走一步

关系定义好了,这一节用它。最小的一步是:给一个对象和一个链接端的名字,取回对面的对象。名字就是你在定义里起的那个,从销售订单出发有 lines、customer、workOrders、shipments。

aidc semantic links salesOrder SO-1001 lines

在 SDK 里是同一件事:

const client = semantic.ontology();
const lines = await client.linked("salesOrder", "SO-1001", "lines");     // a page of order lines
const owner = await client.linked("salesOrder", "SO-1001", "customer");  // at most one customer

指向「一」的端最多返回一个对象,指向「多」的端返回一页。在 /semantic 里,Object View 给每条关系列一个列表,点一个就走到那个对象;本体(Ontology)图里,对象类型是节点,关系是边。

对象集:一批一批地走

真实的问题很少从一个对象开始。「战略客户的在产订单,分别排在哪几条产线上?」要从一批对象出发走好几步。做法是对象集:先取一批、过滤,再沿关系走一步(searchAround,SDK 里叫 pivotTo),得到另一个类型的一批对象,可以接着过滤、接着走。

  1. 01客户过滤:tier 是 strategic
  2. 02订单pivotTo("orders"),再过滤 status
  3. 03工单pivotTo("workOrders")
  4. 04产线pivotTo("line")
const lines = await client.objects("customer")
  .where({ tier: "strategic" })
  .pivotTo("orders")
  .where({ status: "inProduction" })
  .pivotTo("workOrders")
  .pivotTo("line")
  .fetchPage({ $pageSize: 50 });

这条链有三点要记住。每走一步换一次类型,下一步的名字必须是当前类型上的链接端名。结果是集合,同一条产线只算一次。整条链编译成一条 SQL,不是在循环里一个个查,所以放心写长链。

顺序很重要。在 orders 之后过滤 status,问的是「在产的订单」;先走到 workOrders 再过滤工单的 status,问的是「在跑的工单」,两个答案不是一回事。写长链时,把每一步想成一句话:这一批是什么,再决定下一步。

关系也不绕过权限:走到的每个类型都按你的权限检查,看不见的类型,沿关系也走不过去。

最常见的错是把端名用反:orders 是客户上的名字,在订单集上再写 pivotTo("orders"),找不到这条链接;要回到客户,写的是 customer。找不到名字时,用 aidc semantic ontology 看每个类型有哪些链接端。

同样的意思写成 JSON,就是交给 CLI 的对象集。下面是它的第一步:战略客户的订单。

aidc semantic object-set '{"type":"searchAround","objectSet":{"type":"filter","objectSet":{"type":"base","objectType":"customer"},"where":{"type":"eq","field":"tier","value":"strategic"}},"link":"orders"}'

把关系数成数字

关系也能拿来数。派生属性(Derived Property)在读的时候沿关系计算:数个数、求和、取最大最小、收集一个列表。三条规则:最多走 3 跳;只读,不能过滤、不能排序;路上只要有一头是「多」,就必须用聚合。收集列表时,limit 决定最多收几个,缺省 10 个,最多 100 个。

{
  "name": "inspectionCount",
  "type": "integer",
  "title": "Inspections",
  "derived": {
    "linkPath": ["orders", "workOrders", "inspections"],
    "aggregation": { "type": "count" }
  }
}

inspectionCount 走了三跳:客户、订单、工单、检验。它永远和实际一致,不用人维护;代价是每次读都要算。一个经验数字:一次查询涉及一万个对象以内,放心用;再大就先测延迟。

想按条件数,比如「检验失败的有几个」,派生属性做不到,因为它不能过滤。改成先过滤对象集,再聚合。withProperties 则是在一次查询里临时加一个派生属性,不用改定义。

const failed = await client.objects("inspection")
  .where({ result: "fail" })
  .aggregate({ $select: { $count: "unordered" } });

const busy = await client.objects("productionLine")
  .withProperties({ workOrderCount: (b) => b.pivotTo("workOrders").aggregate("$count") })
  .fetchPage();

多对多:读链接上的属性

pivotTo("materials") 取回的是物料对象,里面没有 quantityPerUnit:它属于链接,不属于任何一个对象。链接上的属性在 Semantic 数据库里读。Postgres 方言只为符合条件的多对多关系生成同名视图。两端的 Object Type 都要有视图,关系名不能超过 63 字节。视图两端各一列,列名是「对象类型_链接端名」,例如 product_materials,值是沿这一端走到的对象的主键,其余的列就是链接上的属性。

-- materials needed for one unit of product EB-100, with quantities
SELECT m."name", b."quantityPerUnit"
FROM "productMaterials" b
JOIN "material" m ON m."materialCode" = b."product_materials"
WHERE b."material_products" = 'EB-100';

读的规则和别处一样:一条 SELECT,最多 10,000 行、20 秒,只读。要改这条 BOM,仍然走 Action。

下一节换一种「连接」:不是对象连对象,而是把形状相同的类型放在一起用。

要点

  • 从一个对象走一步:给出对象和链接端的名字,指向「一」返回一个,指向「多」返回一页。
  • 对象集用 pivotTo(searchAround)一批一批地换类型,整条链编译成一条 SQL。
  • 派生属性沿关系读时计算,最多 3 跳,不能过滤、不能排序;要按条件数,先过滤对象集再聚合。
  • 链接自己的属性不在对象上,到 Semantic 数据库的关系视图里读。

练一练

在示例工厂里走一走

有 Semantic 数据的话在真数据上做;没有就先把语句写出来。

用 aidc semantic links 或 SDK 的 linked,从一张销售订单走到它的订单行和客户。哪个端返回一个对象,哪个返回一页?

小测

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

Q1client.objects("customer").pivotTo("orders") 得到的是什么?

Q2想在客户列表里按「在产订单数」排序,能用派生属性吗?

Q3quantityPerUnit 想查出来,去哪里读?

延伸阅读