派生属性

第 6 课 · 共 7 课 约 8 分钟

沿关系读时计算:选择与聚合、最多三跳;不能过滤、不能排序,遇到时怎么办。

本课目标

读完这一课,你将能够

  • 说出派生属性是什么,什么时候该用
  • 写出一个沿关系取值或聚合的派生属性
  • 说出派生属性的限制,遇到限制时知道怎么绕开

读时计算,不存

上一节的值类型,约束的是一个值本身。这一节换一个方向:有些值根本不该存下来,而应该在读的时候沿关系算出来。

客户的订单数,订单的缺陷数,都可以人手维护成一列:每来一张订单加一。可没过几周,数字就和实际对不上了。派生属性(Derived Property)反过来:它不存数据,读的时候沿关系从相连的对象算出来,永远和实际一致。

随口一问

在 customer 上放一个整数 orderCount,每来一张订单由脚本加一。脚本漏跑一次,这个数就永远差一。

好的交代

把 orderCount 写成沿 orders 做 count 的派生属性:每次读到的,都是当时的真实订单数。

派生属性是只读的,人和 Action 都不能写它。它有两种取值方式:

  • 选择(selection):取相连对象的某个属性。只有路径上每一步都是「一」时才能用,例如订单取所属客户的名字。
  • 聚合(aggregation):路径上只要有一步是「多」,就必须聚合,共七种:count、sum、avg、min、max、collectList、collectSet。collectList 保留重复,collectSet 去重,缺省各收 10 个,limit 最多给到 100。

什么时候该用它:值能从关系上直接算出来,又不必按它过滤、排序。如果一个值是人填的,或者要按它排序,就不该是派生属性。

把下面三个派生属性加进 salesOrder 的 columns。linkPath 写的是关系的名字,从当前类型出发,一步一步走:

[
  { "name": "customerName", "type": "string", "title": "Customer name",
    "derived": { "linkPath": ["customer"], "selection": "name" } },
  { "name": "goodQtyTotal", "type": "integer", "title": "Good quantity so far",
    "derived": { "linkPath": ["workOrders"], "aggregation": { "type": "sum", "property": "goodQty" } } },
  { "name": "defectCount", "type": "integer", "title": "Defects",
    "derived": { "linkPath": ["workOrders", "inspections", "defects"], "aggregation": { "type": "count" } } }
]

最多三跳

一跳就是沿一条关系走一步。关系有两端,从哪一端出发,「一」还是「多」不一样:从订单看客户是「一」,从客户看订单是「多」。这决定了该用 selection 还是聚合。

  1. 01销售订单出发的类型 salesOrder
  2. 02工单第 1 跳:workOrders
  3. 03检验第 2 跳:inspections
  4. 04缺陷第 3 跳:defects

订单到缺陷正好三跳,在上限之内,所以 salesOrder.defectCount 能直接写成派生属性。客户的缺陷总数 customer.defectCount 要先走 orders,一共四跳,超出上限,写不了。

这种跨四张表的统计,去 SQL 里做:一对多的关系在 Semantic 数据库里就是直接 join,也不受三跳限制。别为了凑进三跳去改模型,只是偶尔看一眼的数字,让它待在 SQL 里就好。

aidc semantic sql 'SELECT c."customerId", count(d."defectId") AS defects
FROM customer c
JOIN "salesOrder" o ON o."customerId" = c."customerId"
JOIN "workOrder" w ON w."orderNo" = o."orderNo"
JOIN inspection i ON i."workOrderNo" = w."workOrderNo"
JOIN defect d ON d."inspectionId" = i."inspectionId"
GROUP BY 1'

限制清单,和绕开的办法

这些限制大多来自同一个原因:派生属性每次读时才算,不在存储里。过滤、排序和 SQL 的列,都要先有一个存下来的值。

  • 只读:Action 不能写它,也不能标成只在语义层维护(writeback)。
  • 不能做主键,不能必填,不能带值类型,也不能带格式:这些在定义时就会被拒绝。
  • 不能用来过滤,也不能用来排序。
  • 不进 Semantic 数据库:SQL 里没有这一列。
  • 定义里只有路径、聚合和属性,没有筛选条件:想数「还没关闭的缺陷」,派生属性做不到。

遇到这些限制,有三种办法:

SQL

在 SQL 里做

自己 join 和聚合,可以过滤、排序,不受三跳限制。一次一条 SELECT,最多 10,000 行、20 秒。

WITH PROPERTIES

查询里临时加

只在某一次查询里要用,就用 withProperties 临时加上,不用写进类型。

STORED

存成普通属性

一定要按它筛选、排序时,把它存成普通属性,由数据源同步或 Action、工作流来维护,并想办法保证它和实际一致。

withProperties 的写法如下:临时沿 lines 数出每张订单的订单行数。

const client = semantic.ontology();
const withLines = client.objects("salesOrder").withProperties({
  lineCount: (b) => b.pivotTo("lines").aggregate("$count"),
});

派生属性每次读都要算一遍,路径越长、相连的对象越多,越慢。所以读的时候用 --select(SDK 里是 $select)点名要哪几个,没点名的就不会计算:

aidc semantic objects salesOrder --select orderNo,defectCount --page-size 5

下一节讲属性的行为:必填、只在语义层维护、值格式,以及哪些今天有、哪些目前还没有。

要点

  • 派生属性不存数据,读时沿关系算出来,只读,永远和实际一致。
  • 路径上全是「一」用 selection,有「多」就要聚合,共七种。
  • 最多 3 跳:订单到缺陷 3 跳可以,客户到缺陷 4 跳不行。
  • 不能过滤、不能排序、不进 SQL;需要时用 SQL join、withProperties 或存成普通属性。

练一练

给订单和物料加派生属性

销售订单要显示客户名和订单行数,物料要显示在途采购量。

写出 salesOrder.customerName(selection)和 salesOrder.lineCount(沿 lines 数 orderLine 的个数)的 derived 定义。

小测

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

Q1想把 customer.defectCount 写成派生属性,路径是 orders → workOrders → inspections → defects。结果如何?

Q2想在列表里按 orderCount(派生属性)从大到小排序客户。怎么办?

Q3salesOrder.customerName 沿 customer 取客户的名字,该用什么形式?

延伸阅读