返回博客

Ontology 建模最佳实践:6 条黄金法则

在参与了超过 20 个 Ontology 建模项目后,我们总结出一个规律:80% 的建模问题不是技术问题,而是设计决策问题。

Coomia发布于 2025年8月18日23 分钟阅读
分享本文Twitter / X

Ontology 建模最佳实践:6 条黄金法则

系列:S4 本体建模 · 第 14 篇 | 难度:中级 | 阅读时间:18 分钟

#TL;DR

  • 命名规范是可维护性的基石——采用"领域优先、英文 PascalCase、禁止缩写"的原则,让 ObjectType 和属性名自文档化,团队新成员无需翻文档即可理解模型含义。
  • 粒度把控决定了模型的长期生命力——过粗导致属性爆炸、过细导致关系爆炸。黄金法则是"一个 ObjectType 对应一个独立可操作的业务实体"。
  • 关系方向、接口提取、指标设计和版本管理是从"能用"到"好用"的分水岭——这四条法则将你的 Ontology 从 PoC 级别提升到生产级别。

#1. 引言:为什么需要最佳实践

在参与了超过 20 个 Ontology 建模项目后,我们总结出一个规律:80% 的建模问题不是技术问题,而是设计决策问题

常见的"坑"包括:

  • 命名不一致,同一个概念在不同 ObjectType 中叫不同的名字
  • ObjectType 粒度失控,一个"万能对象"塞了 100 个属性
  • RelationType 方向混乱,同一对关系出现了正反两条
  • InterfaceType 没有提取,导致大量属性重复定义
  • 指标定义散落在代码中,无法统一管理和审计
  • Schema 变更没有版本控制,线上频繁出现兼容性问题

这些问题在项目初期看起来无关紧要,但随着模型规模增长到 50+ ObjectType 时,它们会变成技术债务的核心来源。

Code
建模规模         常见问题出现时间线
───────────────────────────────────────────
5 ObjectType     "随便起个名字,先跑起来"
15 ObjectType    "这个属性叫 status 还是 state?"
30 ObjectType    "这两个 ObjectType 有什么区别?"
50 ObjectType    "谁改了 Schema?为什么 API 挂了?"
100 ObjectType   "重构代价太大,只能继续堆"

以下 6 条黄金法则正是为了避免这条"滑坡"而总结的。

#2. 法则一:命名规范——让名字自己说话

#2.1 ObjectType 命名

规则正确示例错误示例原因
PascalCaseProductionOrderproduction_order与 Protobuf/Java 类名一致
名词单数CustomerCustomersObjectType 是类型定义,不是集合
领域名词WorkOrderWO禁止使用缩写
无前缀EquipmentTblEquipment不要暴露存储细节
具体化MaintenanceRecordRecord避免过于泛化
Python
# 好的命名
object_types = [
    "Customer",
    "ProductionOrder",
    "QualityInspection",
    "MaintenanceSchedule",
    "WarehouseLocation",
]

# 坏的命名
bad_names = [
    "Cust",                # 缩写
    "tbl_customer",        # 暴露存储
    "CustomerInfoData",    # 冗余后缀
    "Misc",                # 过于泛化
    "customer",            # 小写开头
]

#2.2 属性命名

规则正确示例错误示例原因
camelCasecreatedAtcreated_atJSON/TypeScript 惯例
有意义的前缀expectedDeliveryDatedate1自文档化
布尔用 is/hasisActiveactive类型自明
避免歧义orderTotalAmounttotal防止跨类型冲突
枚举用名词statusgetStatus属性不是方法
Python
# 属性命名对照表
property_naming = {
    # 时间类
    "createdAt": "TIMESTAMP",      # 创建时间
    "updatedAt": "TIMESTAMP",      # 更新时间
    "scheduledDate": "DATE",       # 计划日期
    "completedAt": "TIMESTAMP",    # 完成时间

    # 布尔类
    "isActive": "BOOLEAN",         # 是否活跃
    "hasWarranty": "BOOLEAN",      # 是否有保修
    "isOverdue": "BOOLEAN",        # 是否逾期

    # 金额类
    "unitPrice": "DOUBLE",         # 单价
    "totalAmount": "DOUBLE",       # 总金额
    "discountRate": "DOUBLE",      # 折扣率

    # 标识类
    "equipmentId": "STRING",       # 设备ID
    "customerId": "STRING",        # 客户ID
    "externalRef": "STRING",       # 外部引用
}

#2.3 RelationType 命名

关系命名采用"动词 + 名词"的模式,方向从源到目标:

Code
命名模式:{Source} --[{Verb}{Target}]--> {Target}

Customer  --[PlacedOrder]-->      Order
Order     --[ContainsProduct]-->  Product
Equipment --[BelongsToLine]-->    ProductionLine
Employee  --[ManagedBy]-->        Employee(自引用)
WorkOrder --[AssignedTo]-->       Equipment
Python
# 关系命名规范
relation_examples = [
    # 从属关系
    RelationType("BelongsToLine",
                 source="Equipment", target="ProductionLine"),
    RelationType("BelongsToDepartment",
                 source="Employee", target="Department"),

    # 操作关系
    RelationType("PlacedOrder",
                 source="Customer", target="Order"),
    RelationType("CreatedInspection",
                 source="Inspector", target="QualityInspection"),

    # 包含关系
    RelationType("ContainsItem",
                 source="Order", target="OrderItem"),
    RelationType("HasComponent",
                 source="Equipment", target="Component"),

    # 引用关系
    RelationType("AssignedTo",
                 source="WorkOrder", target="Equipment"),
    RelationType("ReferencesSupplier",
                 source="PurchaseOrder", target="Supplier"),
]

#2.4 命名自查清单

在每次创建 ObjectType 前,用这个清单自查:

Code
□ 名字是否用了 PascalCase?
□ 名字是否是名词单数形式?
□ 名字是否足够具体(不是 Data、Info、Item 等泛化词)?
□ 名字是否避免了缩写?
□ 名字是否与已有 ObjectType 无歧义?
□ 属性是否用了 camelCase?
□ 布尔属性是否以 is/has 开头?
□ 时间属性是否以 At/Date 结尾?
□ 关系名是否表达了语义方向?

#3. 法则二:粒度把控——一个 ObjectType 一个责任

#3.1 粒度过粗的信号

Code
┌────────────────────────────────────────────────────┐
│              Product (过粗的 ObjectType)              │
│                                                    │
│  基本信息:name, sku, description, category         │
│  价格信息:basePrice, discountPrice, vipPrice       │
│  库存信息:totalStock, availableStock, reservedStock│
│  供应商信息:supplierName, supplierContact          │
│  物流信息:weight, dimensions, shippingClass        │
│  评价信息:avgRating, totalReviews, recentComments  │
│  营销信息:tags, promotionId, bannerUrl             │
│                                                    │
│  属性数量:30+                                      │
│  问题:修改价格策略需要改 Product,                   │
│        修改库存逻辑也需要改 Product                   │
│        → 违反单一职责原则                            │
└────────────────────────────────────────────────────┘

#3.2 粒度过细的信号

Code
┌───────────┐   ┌───────────┐   ┌───────────┐
│ProductName│   │ProductSku │   │ProductDesc│
│           │   │           │   │           │
│ name      │   │ sku       │   │ desc      │
└─────┬─────┘   └─────┬─────┘   └─────┬─────┘
      │               │               │
      └───────────────┼───────────────┘
                      │
                    关系数:3+(仅仅是产品基本信息)
                    问题:查询一个产品需要 JOIN 3 个对象
                          → 过度拆分

#3.3 正确的粒度判断标准

黄金法则:一个 ObjectType 对应一个独立可操作的业务实体。

判断标准:

问题回答"是" → 独立 ObjectType回答"否" → 属性或 StructType
它有独立的生命周期吗?Order 独立于 Customer 存在地址不独立于客户存在
它有独立的主键吗?每个设备有唯一编号设备参数没有独立 ID
业务上可以单独操作它吗?可以单独查询/修改库存不会单独查询产品重量
它会被多个其他对象引用吗?供应商被多个采购单引用订单备注只属于一个订单
它的数据量级值得独立管理吗?传感器读数数百万条产品标签只有几个
Python
# 正确的粒度拆分示例
correct_modeling = {
    "Product": {
        "properties": ["productId", "name", "sku", "description",
                       "category", "weight", "dimensions"],
        "structs": ["Dimensions"],  # 嵌套值对象
    },
    "Pricing": {
        "properties": ["pricingId", "basePrice", "currency",
                       "discountRules", "effectiveFrom", "effectiveTo"],
        "reason": "价格有独立的生效周期和变更审批流程",
    },
    "Inventory": {
        "properties": ["inventoryId", "totalStock", "availableStock",
                       "reservedStock", "warehouseId", "reorderPoint"],
        "reason": "库存有独立的出入库操作和告警规则",
    },
    "Supplier": {
        "properties": ["supplierId", "name", "contact",
                       "rating", "certifications"],
        "reason": "供应商被多个产品/采购单共同引用",
    },
}

#3.4 StructType 的使用时机

当一组属性总是一起出现但不需要独立管理时,使用 StructType:

YAML
# 地址——总是作为其他对象的组成部分
kind: StructType
metadata:
  name: Address
spec:
  properties:
    province:
      type: STRING
    city:
      type: STRING
    district:
      type: STRING
    street:
      type: STRING
    postalCode:
      type: STRING
    coordinates:
      type: STRUCT
      structType: GeoLocation

# 可以在多个 ObjectType 中复用
# Customer.shippingAddress: Address
# Supplier.headquarterAddress: Address
# Warehouse.location: Address

#4. 法则三:关系方向——从"谁拥有谁"到"谁依赖谁"

#4.1 方向选择原则

关系方向不是随意的,它决定了查询的自然性和性能:

Code
原则 1:从"多"指向"一"(BelongsTo 方向)
─────────────────────────────────────────
Order --[PlacedBy]--> Customer      ✅ 自然:订单属于客户
Customer --[HasOrder]--> Order      ⚠️  反向:客户拥有订单(可以,但不是首选)

原因:
- 一个客户可能有 10000 个订单
- 从 Order 出发查 Customer 是 1:1 查询(快)
- 从 Customer 出发查 Order 是 1:N 查询(需要索引)
Code
原则 2:从"依赖方"指向"被依赖方"
─────────────────────────────────────
WorkOrder --[AssignedTo]--> Equipment  ✅ 工单依赖设备
Equipment --[HasWorkOrder]--> WorkOrder ⚠️  设备不依赖工单

原因:
- 删除设备时需要检查是否有未完成的工单
- 依赖方向 = 级联检查方向
Code
原则 3:从"操作发起者"指向"操作目标"
─────────────────────────────────────
Inspector --[PerformedInspection]--> QualityReport  ✅
QualityReport --[InspectedBy]--> Inspector          ⚠️

原因:
- 操作发起者是主语,操作目标是宾语
- 这样的方向与 ActionType 的语义一致

#4.2 避免双向冗余

Code
反模式:
Customer --[HasOrder]-->   Order
Order    --[BelongsTo]--> Customer

问题:
- 两条关系表达同一个语义
- 维护成本翻倍
- 数据不一致的风险

正确做法:
Order --[PlacedBy]--> Customer
(平台自动支持反向遍历,无需声明反向关系)
Python
# 平台支持的反向查询
# 只需声明一条关系
relation = RelationType(
    name="PlacedBy",
    source="Order",
    target="Customer",
    cardinality="MANY_TO_ONE",
)

# 正向查询:从订单找客户
customer = client.ontology.traverse("Order", order_id, "PlacedBy")

# 反向查询:从客户找所有订单(平台自动支持)
orders = client.ontology.traverse_reverse("Customer", customer_id, "PlacedBy")

#4.3 自引用关系

YAML
# 员工的汇报关系
kind: RelationType
metadata:
  name: ReportsTo
spec:
  source: Employee
  target: Employee
  cardinality: MANY_TO_ONE
  properties:
    since:
      type: DATE
    reportType:
      type: STRING
      enum: [DIRECT, DOTTED_LINE]

# 支持的查询:
# - 找某人的直属上级:traverse("Employee", id, "ReportsTo")
# - 找某人的所有下属:traverse_reverse("Employee", id, "ReportsTo")
# - 找整条汇报链:traverse_recursive("Employee", id, "ReportsTo", maxDepth=10)

#4.4 关系方向决策树

Code
Q1: 两个 ObjectType 之间是 1:N 关系吗?
  ├── 是 → 从 N 方指向 1 方(BelongsTo 方向)
  └── 否 → Q2

Q2: 是 M:N 关系吗?
  ├── 是 → 选择"操作发起者"作为 source
  │         (如 Student --[Enrolled]--> Course)
  └── 否 → Q3

Q3: 是 1:1 关系吗?
  ├── 是 → 从"依赖方"指向"被依赖方"
  │         (如 UserProfile --[BelongsTo]--> User)
  └── 否 → 可能不需要 RelationType,考虑嵌入属性

#5. 法则四:接口提取——DRY 原则的 Ontology 版

#5.1 识别重复模式

当你发现多个 ObjectType 共享相同的属性组合时,就是提取 InterfaceType 的信号:

Code
Before(重复属性散布在各处):

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Equipment    │  │  Vehicle     │  │  Building    │
│              │  │              │  │              │
│  createdAt   │  │  createdAt   │  │  createdAt   │
│  createdBy   │  │  createdBy   │  │  createdBy   │
│  updatedAt   │  │  updatedAt   │  │  updatedAt   │
│  updatedBy   │  │  updatedBy   │  │  updatedBy   │
│  status      │  │  status      │  │  status      │
│  ──特有──    │  │  ──特有──    │  │  ──特有──    │
│  model       │  │  plateNumber │  │  floors      │
│  serialNo    │  │  mileage     │  │  area        │
└──────────────┘  └──────────────┘  └──────────────┘

After(提取接口):

┌──────────────────┐     ┌──────────────────┐
│  <<interface>>    │     │  <<interface>>    │
│  Auditable        │     │  Statusable       │
│                  │     │                  │
│  createdAt       │     │  status           │
│  createdBy       │     │                  │
│  updatedAt       │     └──────────────────┘
│  updatedBy       │
└──────────────────┘

Equipment  implements Auditable, Statusable
Vehicle    implements Auditable, Statusable
Building   implements Auditable, Statusable

#5.2 常见的可复用接口

YAML
# 审计接口——谁、在什么时候、做了什么
kind: InterfaceType
metadata:
  name: Auditable
spec:
  properties:
    createdAt: { type: TIMESTAMP }
    createdBy: { type: STRING }
    updatedAt: { type: TIMESTAMP }
    updatedBy: { type: STRING }

---
# 地理位置接口——在哪里
kind: InterfaceType
metadata:
  name: Locatable
spec:
  properties:
    latitude: { type: DOUBLE }
    longitude: { type: DOUBLE }
    address: { type: STRING }
    geofenceId: { type: STRING }

---
# 状态机接口——当前状态和状态变迁
kind: InterfaceType
metadata:
  name: Statusable
spec:
  properties:
    status: { type: STRING }
    statusChangedAt: { type: TIMESTAMP }
    statusChangedBy: { type: STRING }
    previousStatus: { type: STRING }

---
# 可标记接口——分类和标签
kind: InterfaceType
metadata:
  name: Taggable
spec:
  properties:
    tags: { type: ARRAY, itemType: STRING }
    category: { type: STRING }
    labels: { type: MAP, keyType: STRING, valueType: STRING }

---
# 可度量接口——有关联指标
kind: InterfaceType
metadata:
  name: Measurable
spec:
  properties:
    lastMeasuredAt: { type: TIMESTAMP }
    measurementCount: { type: INTEGER }
    measurementSource: { type: STRING }

---
# 可归档接口——软删除和归档
kind: InterfaceType
metadata:
  name: Archivable
spec:
  properties:
    isArchived: { type: BOOLEAN }
    archivedAt: { type: TIMESTAMP }
    archivedBy: { type: STRING }
    archiveReason: { type: STRING }

#5.3 接口的多态查询威力

Python
# 找到所有"在上海"的可定位对象(不管是设备、车辆还是建筑)
result = client.ontology.query(
    interface="Locatable",
    filter="address LIKE '%上海%'",
)

# 返回混合结果:Equipment + Vehicle + Building
for obj in result.objects:
    print(f"[{obj.object_type}] {obj.name} at {obj.address}")

# 找到所有"最近 24 小时被修改"的可审计对象
recent_changes = client.ontology.query(
    interface="Auditable",
    filter="updatedAt > now() - interval('24h')",
    order_by="updatedAt DESC",
)

#5.4 接口提取决策标准

Code
满足以下条件之一即可提取接口:

1. 属性组合在 3+ 个 ObjectType 中重复出现
2. 存在跨类型的多态查询需求
3. 属性组合代表一个独立的业务能力(如"可审计"、"可定位")
4. 需要对异构对象统一应用规则或权限

#6. 法则五:指标设计——从"数据"到"洞察"的桥梁

#6.1 指标定义原则

Code
原则 1:每个指标必须有明确的业务含义
─────────────────────────────────────
✅ "设备综合效率 (OEE)"——管理者能直接理解
❌ "avg_val_123"——无人知道这是什么

原则 2:指标必须声明聚合方式
─────────────────────────────
✅ AVG(oee) GROUP BY productionLine——平均 OEE 按产线
❌ oee——不知道是单值还是聚合

原则 3:指标必须声明维度
─────────────────────────
✅ dimensions: [factory, productionLine, shift]
❌ 没有维度 = 只能看全局值

原则 4:派生指标必须声明依赖
─────────────────────────────
✅ oee = availability * performance * quality
❌ 指标之间的计算关系不透明

#6.2 指标层次设计

Code
Level 4: 战略指标(CEO/CFO)
├── 企业整体 OEE
├── 客户满意度(NPS)
├── 营收达成率
│
Level 3: 运营指标(部门经理)
├── 产线 OEE
├── 订单准时交付率
├── 库存周转率
│
Level 2: 战术指标(班组长)
├── 设备可用率
├── 一次合格率
├── 工单完成率
│
Level 1: 原子指标(传感器/系统)
├── 设备运行时长
├── 产品检测结果
├── 工单状态变更
Python
# Level 1: 原子指标(直接来自数据)
atomic_metrics = [
    MetricSpec(
        name="EquipmentRuntime",
        object_type="Equipment",
        property="runningHours",
        aggregation="SUM",
        unit="hours",
        dimensions=["factory", "productionLine"],
    ),
    MetricSpec(
        name="InspectionPassCount",
        object_type="QualityInspection",
        property="isPassed",
        aggregation="COUNT",
        filter="isPassed == true",
        dimensions=["productionLine", "shift"],
    ),
]

# Level 2: 战术指标(基于原子指标计算)
tactical_metrics = [
    MetricSpec(
        name="EquipmentAvailability",
        expression="EquipmentRuntime / PlannedProductionTime",
        unit="percentage",
        dimensions=["factory", "productionLine"],
    ),
    MetricSpec(
        name="FirstPassYield",
        expression="InspectionPassCount / TotalInspectionCount",
        unit="percentage",
        dimensions=["productionLine", "shift"],
    ),
]

# Level 3: 运营指标(基于战术指标计算)
operational_metrics = [
    MetricSpec(
        name="LineOEE",
        expression="EquipmentAvailability * PerformanceRate * QualityRate",
        unit="percentage",
        dimensions=["factory", "productionLine"],
    ),
]

# Level 4: 战略指标(基于运营指标聚合)
strategic_metrics = [
    MetricSpec(
        name="EnterpriseOEE",
        expression="AVG(LineOEE)",
        unit="percentage",
        dimensions=["factory"],
    ),
]

#6.3 指标告警阈值

Python
# 为指标配置告警
client.metric.set_alert(
    metric="LineOEE",
    rules=[
        AlertRule(
            name="oee-critical",
            condition="value < 0.60",
            severity="CRITICAL",
            message="产线 {productionLine} OEE 低于 60%",
            channels=["sms-factory-manager", "pagerduty"],
        ),
        AlertRule(
            name="oee-warning",
            condition="value < 0.75",
            severity="WARNING",
            message="产线 {productionLine} OEE 低于 75%",
            channels=["slack-production"],
        ),
        AlertRule(
            name="oee-trend-down",
            condition="trend(7d) < -0.05",
            severity="INFO",
            message="产线 {productionLine} OEE 7 天趋势下降 5%",
            channels=["email-production-manager"],
        ),
    ],
)

#6.4 指标版本管理

Python
# 指标定义变更需要经过审批
proposal = client.metric.create_change_proposal(
    metric="LineOEE",
    change_type="FORMULA_UPDATE",
    old_expression="availability * performance * quality",
    new_expression="availability * performance * quality * sustainability",
    reason="加入可持续性因子以符合 ESG 报告要求",
    effective_date="2026-04-01",
)

# 提交审批
proposal.submit_for_review(reviewers=["data-governance-team"])

#7. 法则六:版本管理——Schema 变更的安全网

#7.1 为什么 Schema 版本管理如此重要

Code
没有版本管理的世界:

Day 1: 添加属性 email
Day 3: 将 email 改名为 emailAddress
Day 5: 下游 10 个应用全部报错
Day 6: 紧急回滚,但数据已经不一致了
Day 7: 加班修数据

有版本管理的世界:

Day 1: 创建 Proposal "添加 emailAddress 属性"
Day 2: 自动兼容性检查通过
Day 3: Reviewer 审批通过
Day 4: 灰度发布到 10% 的读取方
Day 5: 全量发布
Day 6: 旧属性 email 标记为 DEPRECATED
Day 30: 确认无人使用后删除旧属性

#7.2 Schema 变更分类

变更类型兼容性自动通过示例
添加可选属性向后兼容新增 nickname: STRING?
添加必选属性(有默认值)向后兼容新增 status: STRING = "ACTIVE"
添加必选属性(无默认值)不兼容新增 email: STRING
删除属性不兼容删除 fax: STRING
修改属性类型不兼容age: STRING → INTEGER
重命名属性不兼容email → emailAddress
添加关系向后兼容新增 BelongsTo
删除关系不兼容删除 BelongsTo

#7.3 Proposal 工作流

Code
┌─────────┐   submit    ┌──────────┐   approve   ┌──────────┐
│  DRAFT   ├────────────►│  REVIEW  ├────────────►│ APPROVED │
└────┬────┘             └────┬─────┘             └────┬─────┘
     │                       │                        │
     │ discard          reject│                   apply│
     │                       │                        │
     ▼                       ▼                        ▼
┌─────────┐           ┌──────────┐            ┌──────────┐
│DISCARDED│           │ REJECTED │            │ APPLIED  │
└─────────┘           └──────────┘            └──────────┘
Python
# 创建 Schema 变更提案
proposal = client.schema.create_proposal(
    title="为 Customer 添加忠诚度相关属性",
    description="支持会员积分系统上线需求",
    changes=[
        AddProperty("Customer", "loyaltyTier",
                    type="STRING", enum=["BRONZE","SILVER","GOLD","PLATINUM"],
                    default="BRONZE"),
        AddProperty("Customer", "loyaltyPoints",
                    type="INTEGER", default=0),
        AddProperty("Customer", "memberSince",
                    type="DATE", nullable=True),
        AddRelation("Customer", "EarnedReward", "Reward",
                    cardinality="ONE_TO_MANY"),
    ],
)

# 提交审批
proposal.submit(reviewers=["schema-admin", "loyalty-team-lead"])

# 查看兼容性检查结果
compat = proposal.compatibility_check()
print(f"Backward compatible: {compat.backward_compatible}")
print(f"Forward compatible: {compat.forward_compatible}")
print(f"Breaking changes: {compat.breaking_changes}")

# 审批通过后应用
proposal.apply(
    rollout_strategy="CANARY",      # 金丝雀发布
    canary_percentage=10,            # 先灰度 10%
    auto_promote_after="24h",        # 24 小时无问题自动全量
)

#7.4 版本号策略

Code
Schema 版本号采用语义化版本:MAJOR.MINOR.PATCH

MAJOR(主版本):不兼容变更
  - 删除属性
  - 修改属性类型
  - 删除关系

MINOR(次版本):向后兼容变更
  - 添加可选属性
  - 添加关系
  - 添加接口实现

PATCH(补丁版本):不影响结构的变更
  - 修改 display_name
  - 修改 description
  - 修改约束(放宽)

示例版本线:
Customer v1.0.0 → v1.1.0 (添加 loyaltyTier) → v1.1.1 (修改描述)
         → v2.0.0 (删除 fax 属性,修改 phone 类型)

#8. 综合实战:应用 6 条法则重构一个 Ontology

#8.1 重构前(违反多条法则)

Python
# 反面教材
bad_ontology = {
    "cust_info": {  # 违反命名规范:缩写、下划线、暴露"info"后缀
        "properties": {
            "id": "STRING",
            "nm": "STRING",            # 缩写
            "created": "STRING",       # 类型错误应该是 TIMESTAMP
            "addr_province": "STRING", # 应该用 StructType
            "addr_city": "STRING",
            "addr_street": "STRING",
            "total_orders": "INTEGER", # 计算逻辑在哪?
            "vip": "BOOLEAN",          # 应该叫 isVip
        },
    },
    "order_data": {  # "data"后缀没有意义
        "properties": {
            "oid": "STRING",
            "cust_id": "STRING",       # 应该是 RelationType
            "items": "STRING",         # JSON 字符串?应该是关系
        },
    },
}

#8.2 重构后(遵循 6 条法则)

Python
# 正面教材

# 法则一:命名规范
# 法则四:接口提取
interfaces = [
    InterfaceType("Auditable", properties=[
        "createdAt", "createdBy", "updatedAt", "updatedBy",
    ]),
    InterfaceType("Archivable", properties=[
        "isArchived", "archivedAt", "archivedBy",
    ]),
]

# 法则二:粒度把控(地址用 StructType)
structs = [
    StructType("Address", properties={
        "province": "STRING",
        "city": "STRING",
        "district": "STRING",
        "street": "STRING",
        "postalCode": "STRING",
    }),
]

# 法则二:粒度把控(独立的业务实体)
object_types = [
    ObjectType("Customer",
        implements=["Auditable", "Archivable"],
        properties={
            "customerId": PropertySpec(type="STRING", primary_key=True),
            "name": PropertySpec(type="STRING", required=True),
            "email": PropertySpec(type="STRING"),
            "shippingAddress": PropertySpec(type="STRUCT",
                                           struct_type="Address"),
            "isVip": PropertySpec(type="BOOLEAN", default=False),
            # 法则五:指标设计——派生属性声明依赖
            "totalOrders": PropertySpec(type="INTEGER", derived=True,
                                       aggregation="COUNT",
                                       source_relation="PlacedOrder"),
        },
    ),
    ObjectType("Order",
        implements=["Auditable"],
        properties={
            "orderId": PropertySpec(type="STRING", primary_key=True),
            "totalAmount": PropertySpec(type="DOUBLE"),
            "status": PropertySpec(type="STRING",
                                   enum=["PENDING","PAID","SHIPPED","COMPLETED"]),
        },
    ),
    ObjectType("OrderItem",
        properties={
            "itemId": PropertySpec(type="STRING", primary_key=True),
            "quantity": PropertySpec(type="INTEGER"),
            "unitPrice": PropertySpec(type="DOUBLE"),
        },
    ),
]

# 法则三:关系方向(从 N 方指向 1 方)
relations = [
    RelationType("PlacedBy", source="Order", target="Customer",
                 cardinality="MANY_TO_ONE"),
    RelationType("ContainsItem", source="Order", target="OrderItem",
                 cardinality="ONE_TO_MANY"),
]

# 法则六:版本管理
proposal = schema.create_proposal(
    title="客户忠诚度体系 v1.1.0",
    changes=[...],
)

#9. 常见反模式清单

反模式描述修正方法
God Object一个 ObjectType 包含 50+ 属性按职责拆分为多个 ObjectType
Anemic ObjectObjectType 只有属性,没有关联 ActionType绑定业务操作
Spaghetti Relations每对 ObjectType 之间都有关系审查必要性,删除冗余关系
Interface Avoidance重复属性散布在各处提取共性为 InterfaceType
Metric Sprawl指标无层次、无治理地增长建立 4 级指标层次
Schema Cowboy直接修改生产 Schema强制使用 Proposal 流程
Name Inconsistency同一概念多个命名建立并维护命名词典
Over-Normalization过度拆分导致查询 N+1合并频繁一起查询的属性

#10. 建模评审 Checklist

在每次 Ontology 建模评审中使用这个 Checklist:

Code
命名规范 (法则一)
□ 所有 ObjectType 使用 PascalCase
□ 所有属性使用 camelCase
□ 无缩写、无前缀、无冗余后缀
□ 布尔属性以 is/has 开头
□ 关系名表达语义方向

粒度把控 (法则二)
□ 每个 ObjectType 对应一个独立业务实体
□ 没有超过 25 个属性的 ObjectType
□ 嵌套值对象使用 StructType
□ 不存在只有 1-2 个属性的 ObjectType

关系方向 (法则三)
□ 1:N 关系从 N 方指向 1 方
□ 不存在双向冗余关系
□ 关系名是"动词+名词"格式
□ 自引用关系有清晰的语义

接口提取 (法则四)
□ 重复出现 3+ 次的属性组合已提取为接口
□ 审计属性已统一为 Auditable 接口
□ 地理位置属性已统一为 Locatable 接口
□ 接口名是形容词(able/ible 后缀)

指标设计 (法则五)
□ 指标有明确的业务含义和单位
□ 指标声明了聚合方式和维度
□ 派生指标声明了依赖关系
□ 关键指标配置了告警阈值

版本管理 (法则六)
□ 所有 Schema 变更通过 Proposal 流程
□ 不兼容变更有迁移计划
□ 版本号遵循语义化版本规范
□ 已废弃属性有明确的删除时间表

#Key Takeaways

  1. 命名规范不是"小事"。在 Ontology 建模中,名字就是 API、就是文档、就是团队沟通的共同语言。PascalCase ObjectType、camelCase 属性、语义化关系名——这三条规则能减少 50% 的沟通成本。

  2. 粒度是"艺术"也是"科学"。用"独立可操作的业务实体"作为判断标准,用 StructType 处理嵌套值对象,既避免了 God Object 又避免了过度拆分。

  3. 关系方向决定查询效率。从"多"指向"一"、从"依赖方"指向"被依赖方"——这两条原则让你的 Ontology 图查询性能提升数倍。

  4. 接口提取是 DRY 原则的 Ontology 版本。Auditable、Locatable、Taggable 等通用接口不仅减少重复,还启用了强大的多态查询能力。

  5. 指标必须分层治理。从原子指标到战略指标的 4 级层次,让每个角色看到自己关心的数据洞察。

  6. Schema 变更必须走 Proposal 流程。兼容性检查、审批、金丝雀发布——这套流程是线上稳定性的最后一道防线。

#下一篇

S4-15: Ontology 实战:电商平台建模 —— 我们将用一个完整的电商场景(Product/Order/User/Inventory/Logistics)来实践以上 6 条黄金法则。

tags: ontology, best-practices, naming-convention, granularity, relation-direction, interface-extraction, metric-design, schema-versioning, coomia-dip