对象集:把问题写成一句话
对象集是一段定义:起点、筛选、并交差、沿关系走、附加派生属性;临时对象集保存 24 小时。

本课目标
读完这一课,你将能够
- 把一个业务问题拆成起点、筛选、组合与沿关系走的几步
- 用
union、intersect、subtract和withProperties表达更复杂的问题 - 说出对象集是一段定义而不是一份数据,以及临时对象集保存多久
对象集是一段定义,不是一份数据
前面几节让数据进来,也让它保持新鲜;从这一节起开始向它提问。对象集描述一组对象:从哪里开始,筛什么,往哪走,和谁合并。它是一小棵 JSON 树,不是查出来的结果;到读取或聚合的时候,才把这棵树编译成一条 SQL 去跑。所以对象集可以随意拼接、传来传去,本身不占数据。
起点
base:一个对象类型的全部对象;interfaceBase:实现某个接口的全部对象;static:一串确定的对象;reference:引用一个已存下来的对象集。
组合
filter:用 where 筛;union:并集;intersect:交集;subtract:差集。
加工
searchAround:沿关系走一步;asType、asBaseObjectTypes:转型;withProperties:临时附加派生属性;nearestNeighbors:找最近邻(第 6 节讲)。
一共十三种,全部编译成一条 SQL,一次读取就出结果。其中 static 是一串确定的对象,之后数据怎么变它都不变;reference 引用的是一个存下来的对象集,读的时候按那个对象集的定义重新算。
先把全部工单读出来,在应用里逐个循环,再一个个去查它的订单、客户。
写成一个对象集:从 workOrder 沿 order 走到订单,再沿 customer 走到客户,一次请求出结果。
把示例工厂的问题写成对象集
问题:关键客户名下、还在生产的订单,它们的全部工单,按交期排。写成对象集,就是从客户出发,筛、走、筛、走四步:筛出关键客户,沿 orders 走到订单,筛出在生产的,再沿 workOrders 走到工单。
const client = semantic.ontology();
const keyOrders = client.objects("customer").where({ tier: "key" }).pivotTo("orders");
const inProduction = keyOrders.where({ status: "inProduction" });
const workOrders = inProduction.pivotTo("workOrders");
const page = await workOrders.fetchPage({ $orderBy: { dueAt: "asc" }, $pageSize: 50 });
// page.data 是这一页;page.totalCount 是一共多少;page.nextPageToken 用来取下一页
pivotTo 就是沿关系走一步:从哪一头出发,就用哪一头的名字。前面几行都只是在拼定义,fetchPage 才真正发出请求。一页最多 1,000 个,asyncIter() 会替你一页一页读完;对象的属性很多时,用 $select 只取要的几个。totalCount 直接告诉你一共有多少,不必翻完所有页才知道。
同一个对象集也能交给 CLI。定义就是那棵树,存成 JSON 文件:
{
"type": "searchAround", "link": "workOrders",
"objectSet": {
"type": "filter",
"where": { "type": "eq", "field": "status", "value": "inProduction" },
"objectSet": {
"type": "searchAround", "link": "orders",
"objectSet": {
"type": "filter",
"where": { "type": "eq", "field": "tier", "value": "key" },
"objectSet": { "type": "base", "objectType": "customer" }
}
}
}
}
然后把文件交给 aidc semantic object-set,读一页,或者只数个数:
aidc semantic object-set key-work-orders.json --page-size 50
aidc semantic object-set key-work-orders.json --aggregate '{"$select":{"$count":"unordered"}}'
合并、扣除与附加
union:属于任何一组的对象。intersect:同时属于每一组的对象。subtract:属于第一组、不属于其余各组的对象。
「已确认、但还没有工单的订单」,就是先取已确认的订单,再扣掉已经有工单的那些。它正好是示例工厂第一条自动化的起点:订单确认后,去建工单。
const confirmed = client.objects("salesOrder").where({ status: "confirmed" });
const hasWorkOrders = client.objects("workOrder").pivotTo("order");
const waiting = confirmed.subtract(hasWorkOrders); // 已确认、还没有工单
// 给每个客户临时加两个派生属性:订单数、订单总额
const customers = client.objects("customer").withProperties({
orderCount: (b) => b.pivotTo("orders").aggregate("$count"),
orderTotalUsd: (b) => b.pivotTo("orders").aggregate("totalUsd:sum"),
});
const page = await customers.fetchPage();
intersect 的用法类似。「关键客户名下、而且有工单被暂停的订单」,就是两个对象集的交集:一个从暂停的工单走回订单,一个从关键客户走到订单,只留两边都有的。同一个问题,应用用 SDK 写,智能体用 CLI 写,写出来的是同一棵树。
const pausedOrders = client.objects("workOrder").where({ status: "paused" }).pivotTo("order");
const keyCustomerOrders = client.objects("customer").where({ tier: "key" }).pivotTo("orders");
const escalate = pausedOrders.intersect(keyCustomerOrders); // 关键客户、且有工单被暂停的订单
withProperties 加上的值是读的时候沿关系算的,最多沿 3 跳,而且只算这一页要显示的对象。代价是它们不能用来过滤,也不能排序。要按订单数给客户排名,就对订单按 customerId 分组计数(下一节),或者用 SQL 连表。
对象集的起点也可以是接口。Schedulable 由销售订单、工单和采购订单共同实现,把它们当成一组,就能跨类型提问,属性用接口里的名字(dueAt、status):
// 三类对象合成一组:按状态数一数
const byStatus = await client.interface("Schedulable").aggregate({
$select: { $count: "unordered" },
$groupBy: { status: "exact" },
});
asType 能把这样的对象集收窄到某一个对象类型,asBaseObjectTypes 能让对象换回自己类型的属性名。这两个目前只能写在对象集的 JSON 定义里,SDK 还没有对应的方法。
对象集可以存下来。createTemporary() 存成临时对象集,返回一个 objectSetRid,保存 24 小时,过期就读不到了;想长期留着,就保存成有名字的对象集。存下来的是定义,不是当时的结果:以后每次读,都按最新的数据重新算。
const { objectSetRid } = await workOrders.createTemporary(); // 24 小时内有效
const again = client.objectSet(objectSetRid); // 引用它
const n = await again.aggregate({ $select: { $count: "unordered" } }); // { $count: … }
下一节讲对象集里 where 怎么写,以及聚合怎样回答「多少」。
要点
- 对象集是一段定义:起点加筛选、组合和沿关系走的几步,读取时才编译成一条 SQL。
pivotTo沿关系走一步,从哪一头出发就用哪一头的名字。union、intersect、subtract表达并、交、差;「有 A 但没有 B」用subtract。withProperties读时沿关系算,最多 3 跳,只用来读,不能过滤和排序。- 临时对象集保存 24 小时,存下来的是定义,不是当时的结果。
练一练
在示例工厂里写三个对象集
用蓝图里的对象类型和关系名,先写 JS,再挑一个改写成 JSON 交给 CLI。
写出「已确认、但还没有工单的订单」;再写出「有工单、但还没有发运的订单」(提示:从订单出发的关系叫 shipments)。
写出「评分低于 3 的供应商的全部采购订单」,再用 aidc semantic object-set 跑一遍并加上 --aggregate 数出个数。
要把客户按「未结订单数」排名,withProperties 能直接做吗?如果不能,你会用什么?
小测
选一个答案,马上看解析。
Q1createTemporary() 存下来的对象集,保存的是什么?
对象集本来就是定义。存下来的是同一段定义,保存 24 小时;数据变了,再读到的结果也会变。
Q2「已确认、但还没有工单的订单」应该怎么写?
要的是第一组里、不在第二组的对象,这正是 subtract。union 会把两组都要,intersect 只留下有工单的。
Q3想把客户按「订单数」从多到少排名,下面哪种做法行得通?
派生出来的值是读的时候算的,不能排序;聚合和 SQL 是有人问时一起算,永远和实际一致,不需要手工维护。