Ontology 建模最佳实践:6 条黄金法则
在参与了超过 20 个 Ontology 建模项目后,我们总结出一个规律:80% 的建模问题不是技术问题,而是设计决策问题。
Ontology 建模最佳实践:6 条黄金法则
“系列:S4 本体建模 · 第 14 篇 | 难度:中级 | 阅读时间:18 分钟
#TL;DR
- 命名规范是可维护性的基石——采用"领域优先、英文 PascalCase、禁止缩写"的原则,让 ObjectType 和属性名自文档化,团队新成员无需翻文档即可理解模型含义。
- 粒度把控决定了模型的长期生命力——过粗导致属性爆炸、过细导致关系爆炸。黄金法则是"一个 ObjectType 对应一个独立可操作的业务实体"。
- 关系方向、接口提取、指标设计和版本管理是从"能用"到"好用"的分水岭——这四条法则将你的 Ontology 从 PoC 级别提升到生产级别。
#1. 引言:为什么需要最佳实践
在参与了超过 20 个 Ontology 建模项目后,我们总结出一个规律:80% 的建模问题不是技术问题,而是设计决策问题。
常见的"坑"包括:
- 命名不一致,同一个概念在不同 ObjectType 中叫不同的名字
- ObjectType 粒度失控,一个"万能对象"塞了 100 个属性
- RelationType 方向混乱,同一对关系出现了正反两条
- InterfaceType 没有提取,导致大量属性重复定义
- 指标定义散落在代码中,无法统一管理和审计
- Schema 变更没有版本控制,线上频繁出现兼容性问题
这些问题在项目初期看起来无关紧要,但随着模型规模增长到 50+ ObjectType 时,它们会变成技术债务的核心来源。
建模规模 常见问题出现时间线
───────────────────────────────────────────
5 ObjectType "随便起个名字,先跑起来"
15 ObjectType "这个属性叫 status 还是 state?"
30 ObjectType "这两个 ObjectType 有什么区别?"
50 ObjectType "谁改了 Schema?为什么 API 挂了?"
100 ObjectType "重构代价太大,只能继续堆"
以下 6 条黄金法则正是为了避免这条"滑坡"而总结的。
#2. 法则一:命名规范——让名字自己说话
#2.1 ObjectType 命名
| 规则 | 正确示例 | 错误示例 | 原因 |
|---|---|---|---|
| PascalCase | ProductionOrder | production_order | 与 Protobuf/Java 类名一致 |
| 名词单数 | Customer | Customers | ObjectType 是类型定义,不是集合 |
| 领域名词 | WorkOrder | WO | 禁止使用缩写 |
| 无前缀 | Equipment | TblEquipment | 不要暴露存储细节 |
| 具体化 | MaintenanceRecord | Record | 避免过于泛化 |
# 好的命名
object_types = [
"Customer",
"ProductionOrder",
"QualityInspection",
"MaintenanceSchedule",
"WarehouseLocation",
]
# 坏的命名
bad_names = [
"Cust", # 缩写
"tbl_customer", # 暴露存储
"CustomerInfoData", # 冗余后缀
"Misc", # 过于泛化
"customer", # 小写开头
]
#2.2 属性命名
| 规则 | 正确示例 | 错误示例 | 原因 |
|---|---|---|---|
| camelCase | createdAt | created_at | JSON/TypeScript 惯例 |
| 有意义的前缀 | expectedDeliveryDate | date1 | 自文档化 |
| 布尔用 is/has | isActive | active | 类型自明 |
| 避免歧义 | orderTotalAmount | total | 防止跨类型冲突 |
| 枚举用名词 | status | getStatus | 属性不是方法 |
# 属性命名对照表
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 命名
关系命名采用"动词 + 名词"的模式,方向从源到目标:
命名模式:{Source} --[{Verb}{Target}]--> {Target}
Customer --[PlacedOrder]--> Order
Order --[ContainsProduct]--> Product
Equipment --[BelongsToLine]--> ProductionLine
Employee --[ManagedBy]--> Employee(自引用)
WorkOrder --[AssignedTo]--> Equipment
# 关系命名规范
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 前,用这个清单自查:
□ 名字是否用了 PascalCase?
□ 名字是否是名词单数形式?
□ 名字是否足够具体(不是 Data、Info、Item 等泛化词)?
□ 名字是否避免了缩写?
□ 名字是否与已有 ObjectType 无歧义?
□ 属性是否用了 camelCase?
□ 布尔属性是否以 is/has 开头?
□ 时间属性是否以 At/Date 结尾?
□ 关系名是否表达了语义方向?
#3. 法则二:粒度把控——一个 ObjectType 一个责任
#3.1 粒度过粗的信号
┌────────────────────────────────────────────────────┐
│ 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 粒度过细的信号
┌───────────┐ ┌───────────┐ ┌───────────┐
│ProductName│ │ProductSku │ │ProductDesc│
│ │ │ │ │ │
│ name │ │ sku │ │ desc │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
关系数:3+(仅仅是产品基本信息)
问题:查询一个产品需要 JOIN 3 个对象
→ 过度拆分
#3.3 正确的粒度判断标准
黄金法则:一个 ObjectType 对应一个独立可操作的业务实体。
判断标准:
| 问题 | 回答"是" → 独立 ObjectType | 回答"否" → 属性或 StructType |
|---|---|---|
| 它有独立的生命周期吗? | Order 独立于 Customer 存在 | 地址不独立于客户存在 |
| 它有独立的主键吗? | 每个设备有唯一编号 | 设备参数没有独立 ID |
| 业务上可以单独操作它吗? | 可以单独查询/修改库存 | 不会单独查询产品重量 |
| 它会被多个其他对象引用吗? | 供应商被多个采购单引用 | 订单备注只属于一个订单 |
| 它的数据量级值得独立管理吗? | 传感器读数数百万条 | 产品标签只有几个 |
# 正确的粒度拆分示例
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:
# 地址——总是作为其他对象的组成部分
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 方向选择原则
关系方向不是随意的,它决定了查询的自然性和性能:
原则 1:从"多"指向"一"(BelongsTo 方向)
─────────────────────────────────────────
Order --[PlacedBy]--> Customer ✅ 自然:订单属于客户
Customer --[HasOrder]--> Order ⚠️ 反向:客户拥有订单(可以,但不是首选)
原因:
- 一个客户可能有 10000 个订单
- 从 Order 出发查 Customer 是 1:1 查询(快)
- 从 Customer 出发查 Order 是 1:N 查询(需要索引)
原则 2:从"依赖方"指向"被依赖方"
─────────────────────────────────────
WorkOrder --[AssignedTo]--> Equipment ✅ 工单依赖设备
Equipment --[HasWorkOrder]--> WorkOrder ⚠️ 设备不依赖工单
原因:
- 删除设备时需要检查是否有未完成的工单
- 依赖方向 = 级联检查方向
原则 3:从"操作发起者"指向"操作目标"
─────────────────────────────────────
Inspector --[PerformedInspection]--> QualityReport ✅
QualityReport --[InspectedBy]--> Inspector ⚠️
原因:
- 操作发起者是主语,操作目标是宾语
- 这样的方向与 ActionType 的语义一致
#4.2 避免双向冗余
反模式:
Customer --[HasOrder]--> Order
Order --[BelongsTo]--> Customer
问题:
- 两条关系表达同一个语义
- 维护成本翻倍
- 数据不一致的风险
正确做法:
Order --[PlacedBy]--> Customer
(平台自动支持反向遍历,无需声明反向关系)
# 平台支持的反向查询
# 只需声明一条关系
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 自引用关系
# 员工的汇报关系
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 关系方向决策树
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 的信号:
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 常见的可复用接口
# 审计接口——谁、在什么时候、做了什么
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 接口的多态查询威力
# 找到所有"在上海"的可定位对象(不管是设备、车辆还是建筑)
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 接口提取决策标准
满足以下条件之一即可提取接口:
1. 属性组合在 3+ 个 ObjectType 中重复出现
2. 存在跨类型的多态查询需求
3. 属性组合代表一个独立的业务能力(如"可审计"、"可定位")
4. 需要对异构对象统一应用规则或权限
#6. 法则五:指标设计——从"数据"到"洞察"的桥梁
#6.1 指标定义原则
原则 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 指标层次设计
Level 4: 战略指标(CEO/CFO)
├── 企业整体 OEE
├── 客户满意度(NPS)
├── 营收达成率
│
Level 3: 运营指标(部门经理)
├── 产线 OEE
├── 订单准时交付率
├── 库存周转率
│
Level 2: 战术指标(班组长)
├── 设备可用率
├── 一次合格率
├── 工单完成率
│
Level 1: 原子指标(传感器/系统)
├── 设备运行时长
├── 产品检测结果
├── 工单状态变更
# 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 指标告警阈值
# 为指标配置告警
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 指标版本管理
# 指标定义变更需要经过审批
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 版本管理如此重要
没有版本管理的世界:
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 工作流
┌─────────┐ submit ┌──────────┐ approve ┌──────────┐
│ DRAFT ├────────────►│ REVIEW ├────────────►│ APPROVED │
└────┬────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ discard reject│ apply│
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│DISCARDED│ │ REJECTED │ │ APPLIED │
└─────────┘ └──────────┘ └──────────┘
# 创建 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 版本号策略
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 重构前(违反多条法则)
# 反面教材
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 条法则)
# 正面教材
# 法则一:命名规范
# 法则四:接口提取
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 Object | ObjectType 只有属性,没有关联 ActionType | 绑定业务操作 |
| Spaghetti Relations | 每对 ObjectType 之间都有关系 | 审查必要性,删除冗余关系 |
| Interface Avoidance | 重复属性散布在各处 | 提取共性为 InterfaceType |
| Metric Sprawl | 指标无层次、无治理地增长 | 建立 4 级指标层次 |
| Schema Cowboy | 直接修改生产 Schema | 强制使用 Proposal 流程 |
| Name Inconsistency | 同一概念多个命名 | 建立并维护命名词典 |
| Over-Normalization | 过度拆分导致查询 N+1 | 合并频繁一起查询的属性 |
#10. 建模评审 Checklist
在每次 Ontology 建模评审中使用这个 Checklist:
命名规范 (法则一)
□ 所有 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
-
命名规范不是"小事"。在 Ontology 建模中,名字就是 API、就是文档、就是团队沟通的共同语言。PascalCase ObjectType、camelCase 属性、语义化关系名——这三条规则能减少 50% 的沟通成本。
-
粒度是"艺术"也是"科学"。用"独立可操作的业务实体"作为判断标准,用 StructType 处理嵌套值对象,既避免了 God Object 又避免了过度拆分。
-
关系方向决定查询效率。从"多"指向"一"、从"依赖方"指向"被依赖方"——这两条原则让你的 Ontology 图查询性能提升数倍。
-
接口提取是 DRY 原则的 Ontology 版本。Auditable、Locatable、Taggable 等通用接口不仅减少重复,还启用了强大的多态查询能力。
-
指标必须分层治理。从原子指标到战略指标的 4 级层次,让每个角色看到自己关心的数据洞察。
-
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