# Object：从表到对象

**Object（对象）是 Semantic 里企业数据的基本单位**：一张订单、一种物料、一个工单、一位客户。它不是数据库里的一行，而是一件业务里的东西——有名字、有属性、和别的对象有关系、能被受控地修改、变了之后会自动触发下一步。人、应用和智能体读的是同一份对象。

这一页讲清四件事：对象是什么、一个对象由哪几部分组成、怎样从零构建一个对象、和原来直接查表的做法比有什么不同。每一部分的完整写法在 [Semantic](semantic.md)，数据怎么进来见 [数据管道](pipeline.md)，一整套交付方法见 Academy 的 [FDE 课程](https://www.ai-dc.ai/academy/fde)。示例取自虚构的 Demo Company（一家生产工业泵与阀门的制造企业，数据全部虚构）。

```
ERP / MES / OA（只读）──▶ 数据管道 ──▶ Object Type（属性 · 关系）──▶ Action · Function · Automation
                                              │
                                              └──▶ 应用 · 智能体 · SQL · 订阅（读同一份对象）
```

## 1. 对象是什么

| | 数据库里的一行 | Semantic 里的一个对象 |
| --- | --- | --- |
| 名字 | 表名、列名照源系统（`dbo.SalesOrderLine`、`dueDt`） | 业务名字：`SalesOrderLine` 订单行、`dueDate` 交期，带单位与同义词 |
| 口径 | 写在每个查询、每个脚本里，各写一遍 | 写在对象类型的定义里，一处定义、处处一致 |
| 关系 | 靠 join，谁写查询谁知道 | Link Type 两端都有名字：`order.lines`、`line.order` |
| 改数据 | 回写源系统，或者另建一张表 | 成员和智能体只走 Action：参数校验、提交条件、谁能做、留痕 |
| 权限 | 按库、按表开账号 | 按对象类型、按行、按列，读的人是谁就按谁判定 |
| 变了做什么 | 定时任务每隔几分钟查一遍 | Automation 在对象变化时触发 |
| 谁来读 | 会写 SQL 的人 | 人（Object Explorer、应用）、智能体（MCP、CLI）、程序（SDK、SQL） |

对象由 **Object Type（对象类型）** 定义：一类对象的主键、标题和属性。对象的值从数据源同步进来（按设计不由动作改），人和智能体的改动经 Action 落在语义层，两层叠起来就是对象的当前值。

## 2. 一个对象的组成

一个对象 = **名词**（它是什么、和谁有关）+ **动力**（它能做什么、变了之后做什么）。以 Demo Company 的订单行 `SalesOrderLine` 为例：

| 部分 | 构件 | 回答什么问题 | `SalesOrderLine` 的例子 |
| --- | --- | --- | --- |
| 名词 | **Object Type** | 这一类东西是什么、用什么认出它 | 主键 `lineId`（`<订单号>-<行号>`），标题 `productCode` |
| | **Property** | 它有哪些特征，各是什么类型 | `qty`（integer）、`dueDate`（date）、`status`（值类型 `orderStatus`）、`promisedDate`（只在语义层维护） |
| | **Link Type** | 它和谁有关 | `order`（所属订单）、`workOrders`（生产它的工单） |
| | **Interface** | 它和别的类型有什么共同的形状 | 实现 `Schedulable`（`dueDate`、`status`），和工单、采购订单一起查「今天到期」 |
| | **Shared Property / Value Type** | 哪些字段跨类型复用、取值有什么约束 | `dueDate` 是共享属性；`orderStatus` 只能是 已下单 / 已排产 / 生产中 / 待发货 / 已发货 / 已关闭 |
| 动力 | **Derived Property** | 哪些值读的时候算 | 沿关系数出工单数（最多 3 跳；派生属性不带筛选条件，「未完成的工单数」这种要筛选的写成函数） |
| | **Action** | 谁能怎样改它 | `confirm-delivery-date`：确认交期，只能由人执行，开 Action Log |
| | **Function** | 复杂的逻辑怎么算 | `deliveryRisk`：按距交期天数、未关闭缺料、工单进度算风险 |
| | **Automation** | 它变了、或者到点了，自动做什么 | `overdue-lines-daily`：工作日 08:30，交期已到而未发货的订单行给营业负责人发邮件 |

动力部分让对象「会做事」：只能看的对象只是报表；能被受控地改、能自动触发下一步，对象才进入业务流程。

## 3. 从零构建一个对象

下面八步在 Demo Company 上建出 `SalesOrderLine`。命令要登录（`aidc login`）并有本组织的开发者权限；智能体作者走第 8 步的分支与提案。

### ① 从决策出发

先列出业务每周要做的决策，再倒推需要的对象：营业员每天要「确认交期」，计划员每天要「决定先做哪张工单」——于是需要订单行、工单、物料、缺料四个对象，以及确认交期、登记缺料两个 Action。不要从 ERP 的表清单出发：表是系统的形状，对象是业务的形状。

### ② 定义对象类型与属性

一个 Object Type 恰好一个主键属性（复合键在数据源里拼成一列）；名字照规范：Object Type 用 PascalCase，属性用 camelCase。

```json
{
  "kind": "objectType",
  "apiName": "SalesOrderLine",
  "title": "订单行",
  "schema": {
    "titleColumn": "productCode",
    "columns": [
      { "name": "lineId", "type": "string", "primaryKey": true, "title": "订单行" },
      { "name": "orderNo", "type": "string", "title": "订单号" },
      { "name": "productCode", "type": "string", "title": "产品" },
      { "name": "qty", "type": "integer", "title": "数量", "unit": "台" },
      { "name": "dueDate", "type": "date", "title": "交期", "synonyms": ["交货日期", "要货日期"] },
      { "name": "status", "type": "string", "title": "状态" },
      { "name": "promisedDate", "type": "date", "title": "承诺交期", "writeback": true }
    ]
  }
}
```

`writeback: true` 的属性只在语义层维护：它不从数据源来，由 Action 写。其余属性来自数据源，Action 的规则不要写它们：平台会给设计警告；写了，这笔改动会一直盖住之后同步来的值。

### ③ 接上数据

数据源写在对象类型定义的 `datasources` 里——改数据源就是改本体。Demo Company 的 ERP 在工厂内网，用 Data Connection 读：每张表一个 TableImport，结果写成 Dataset，再映射到属性（完整做法见 [数据管道](pipeline.md)）：

```json
"datasources": [
  { "type": "dataset", "dataset": "sales-order-lines",
    "propertyMapping": { "lineId": "line_id", "orderNo": "order_no", "productCode": "product_code", "qty": "qty", "dueDate": "due_date", "status": "status" } }
]
```

主键必须映射；只在语义层维护的属性（`promisedDate`）和派生属性不映射。定义落了之后平台先全量同步一次，之后只把变了的行写成对象。

### ④ 建关系

一对多用外键，两端各起名字：

```json
{
  "kind": "linkType",
  "apiName": "salesOrderLines",
  "title": "订单的订单行",
  "schema": {
    "from": "SalesOrder", "to": "SalesOrderLine", "cardinality": "ONE_TO_MANY",
    "apiNameAtoB": "lines", "apiNameBtoA": "order",
    "foreignKey": { "side": "to", "property": "orderNo" }
  }
}
```

多对多每条链接单独存；关系如果带状态、要审批、有两个以上属性，升级成中间对象（例：缺料不是物料和工单之间的关系，而是有负责人、有状态的 `Shortage` 对象）。

### ⑤ 定义 Action

成员和智能体改数据只走 Action：参数、规则、提交条件、留痕都写在定义里（直接写数据的接口只给开发者）。

```json
{
  "kind": "actionType",
  "apiName": "confirm-delivery-date",
  "title": "确认交期",
  "schema": {
    "parameters": [
      { "name": "line", "type": "object", "objectType": "SalesOrderLine", "required": true },
      { "name": "promisedDate", "type": "date", "required": true }
    ],
    "rules": [{ "type": "modifyObject", "objectType": "SalesOrderLine", "object": "$line", "values": { "promisedDate": "$promisedDate" } }],
    "submissionCriteria": [{
      "condition": { "type": "comparison", "left": { "user": "authorKind" }, "operator": "is", "right": { "literal": "human" } },
      "failureMessage": "交期是对客户的承诺，只能由人确认"
    }],
    "actionLog": true
  }
}
```

提交条件里比较的值可以是参数（对象参数可带 `property`）、常量，或执行人（`{ "user": "id" | "name" | "role" | "authorKind" }`）。「只有人能确认」用肯定的写法 `authorKind is human`；用了 `authorKind` 的 Action 暂时只能经 v1 接口、CLI 或 Semantic 界面执行。先只校验再执行：`client.action("confirm-delivery-date").applyAction({…}, { $validateOnly: true })`。

### ⑥ 写函数（需要时）

派生属性够用就不写函数（沿关系 count / sum / avg / min / max，最多 3 跳）。更复杂的逻辑写成函数：一个 ES module，默认导出 `(params, ctx) => 结果`，`ctx` 读对象；语义化版本，发布后不能改。

```bash
aidc semantic functions publish delivery-risk.js --api-name deliveryRisk --version 1.0.0 \
  --output '{"type":"integer"}' --parameters '{"lineId":{"dataType":{"type":"string"}}}'
```

### ⑦ 加自动化

数据一变或者到点就做事，不写定时任务。「交期已到而未发货」是因为时间过去才进集合的，用 **Run on all objects**；新出现的缺料用 **Objects added**。效果可以是 Action、Function 或邮件通知，详见 [Semantic](semantic.md)「自动化」。

```bash
aidc semantic automations upsert --ontology cell-demo --file overdue-lines-daily.json --dry-run
aidc semantic automations upsert --ontology cell-demo --file overdue-lines-daily.json
```

### ⑧ 提交、验证、发版本

```bash
aidc semantic define ontology/ --dry-run       # 整批校验：引用是否闭合、相对上一个语义版本有哪些破坏性变更
aidc semantic define ontology/
aidc semantic publish --notes "订单行、确认交期"
aidc semantic objects SalesOrderLine --where '{"status":"生产中"}' --json
```

智能体不能直接改 main：它在分支上改、开提案，人在网页上逐项批准后合并（`aidc semantic branch create / modify / propose`，见 [智能体接入](agents.md)）。

## 4. 和原来的做法比

| | 原来：直接查表、定时刷新 | 现在：Semantic 对象 |
| --- | --- | --- |
| 数据怎么来 | 每个看板、每个脚本各查一遍 ERP | 源头只读一次，只把变了的行写成对象 |
| 什么时候算 | 定时任务每 5–10 分钟跑一次，没人看也跑 | 有人读、数据过期才同步（自动化引用的类型按评估频率）；应用没人打开不计算 |
| 看板怎么发 | 生成整页 HTML 再上传；内网页面靠临时隧道从外网打开 | Applications 只存定义，打开时按看的人的权限现算 |
| 口径 | 散在各个脚本里，改一处漏一处 | 写在对象类型里，人、应用、智能体同一份 |
| 改数据 | 回写源系统，或者在看板旁边另记一张表 | Action：校验、权限、Action Log，同步不会冲掉人的改动 |
| 数据是不是新的 | 脚本断了也没人知道，看板静默变旧 | 类型带同步状态（最近同步时间），应用有数据新鲜度组件；数据健康检查失败会告警 |
| 智能体 | 拿库的账号自己写 SQL，读到不该读的 | 读对象集、改数据只走 Action，权限按对象判定 |

一个每 10 分钟刷新一次的看板，一天跑 144 次（1440 ÷ 10）；24 个这样的脚本一天 3,456 次（示例）。按需之后，同一类数据一小时最多同步 3600 ÷ 300 = 12 次（`freshness.maxAgeSeconds` 缺省 300 秒），而且只在有人读的时段发生。账怎么算、怎么迁，见 [数据管道](pipeline.md)。

## 5. 规则与上限

- 一个 Object Type 恰好一个主键属性；Object Type 名在本体里不分大小写唯一，`ontology`、`object`、`link` 这类保留字不能用。
- 名字：Object Type 与 Interface 用 PascalCase；Property、Link 两端、Value Type、Shared Property 用 camelCase；Action Type 用 kebab-case。
- 派生属性最多沿关系 3 跳；派生属性不进 Semantic SQL。
- 定义里写了不认识的字段直接被拒，错误信息给出路径。
- 数据流的字段白名单里，证件号、银行卡、密码这类字段直接拒收（见[连接](connect.md)）。
- 数据源永远只读；要真的改源系统，给 Action 配 writeback webhook（源系统先接受，语义层才记一笔）。
- 敏感数据用 Markings、对象安全策略（行）与属性安全策略（列）管住，见 [Semantic](semantic.md)「数据安全」。

## 6. 常见问题

**对象和数据库表一一对应吗？** 不一定。一个对象类型可以汇集几张表（订单头 + 订单行），一张表也可能拆成几个对象类型。按业务里的一件东西建，不按表建。

**已经有 ERP 报表了，为什么还要建对象？** 报表回答「表里有什么」，对象回答「这件东西现在是什么状态、归谁、和谁有关、下一步谁来做」。报表不能被受控地修改，也不会在变化时触发下一步。

**建多少个对象类型合适？** 从支撑决策的核心对象起步（常见是 8 个左右），按真实需要扩展。在用的不完美本体好过设计中的完美本体。

**对象的数据多久更新一次？** 由数据源决定：数据流的发布端有变化就推；Data Connection 在有人读、数据比 `freshness.maxAgeSeconds` 旧的时候同步，`?fresh=true` 最多等 20 秒，到时还没同步完就先返回现有的数据。
