对象集:把问题写成一句话

第 4 课 · 共 6 课 约 8 分钟

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

本课目标

读完这一课,你将能够

  • 把一个业务问题拆成起点、筛选、组合与沿关系走的几步
  • 用 union、intersect、subtract 和 withProperties 表达更复杂的问题
  • 说出对象集是一段定义而不是一份数据,以及临时对象集保存多久

对象集是一段定义,不是一份数据

前面几节让数据进来,也让它保持新鲜;从这一节起开始向它提问。对象集描述一组对象:从哪里开始,筛什么,往哪走,和谁合并。它是一小棵 JSON 树,不是查出来的结果;到读取或聚合的时候,才把这棵树编译成一条 SQL 去跑。所以对象集可以随意拼接、传来传去,本身不占数据。

START

起点

base:一个对象类型的全部对象;interfaceBase:实现某个接口的全部对象;static:一串确定的对象;reference:引用一个已存下来的对象集。

COMBINE

组合

filter:用 where 筛;union:并集;intersect:交集;subtract:差集。

SHAPE

加工

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)。

小测

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

Q1createTemporary() 存下来的对象集,保存的是什么?

Q2「已确认、但还没有工单的订单」应该怎么写?

Q3想把客户按「订单数」从多到少排名,下面哪种做法行得通?

延伸阅读