ObjectType 深度解析:属性类型系统与约束体系
在传统数据库中,你只有 VARCHAR、INT、DECIMAL、TIMESTAMP 等有限类型。当业务需要表达"这个字段只能是 3 个值之一"或"这个字段是一个嵌套的地址结构"时,你不得不在应用层处理。
ObjectType 深度解析:属性类型系统与约束体系
“系列:S4 本体建模 · 第 2 篇 | 难度:中级 | 阅读时间:18 分钟
#TL;DR
- coomia-dip 提供 20+ 种属性类型,从基础标量(STRING / INTEGER / DOUBLE)到复合类型(STRUCT / ARRAY / MAP / ENUM),覆盖工业级建模的所有需求。
- 约束体系支持三个层次:字段级(required / unique / range / pattern)、对象级(跨字段规则)、关系级(外键参照完整性),让数据质量在 Schema 层而非应用层保障。
- 主键设计有 4 种策略(自然键 / UUID / 复合键 / 序列键),选择不当会导致数据分布倾斜、查询性能下降,本文给出决策矩阵。
#1. 为什么属性类型系统如此重要
在传统数据库中,你只有 VARCHAR、INT、DECIMAL、TIMESTAMP 等有限类型。当业务需要表达"这个字段只能是 3 个值之一"或"这个字段是一个嵌套的地址结构"时,你不得不在应用层处理。
coomia-dip 的 ObjectType 将类型检查前移到 Schema 层:
传统方式 coomia-dip 方式
┌─────────────┐ ┌──────────────────────┐
│ Database │ │ SchemaRegistry │
│ VARCHAR(50) │ → 运行时检查 │ STRING(maxLen=50) │ → 定义时检查
│ INT │ 应用层验证 │ ENUM(A,B,C) │ Schema 验证
│ TEXT(JSON) │ 无结构保证 │ STRUCT(Address) │ 结构保证
└─────────────┘ └──────────────────────┘
#2. 完整属性类型清单
#2.1 标量类型(Scalar Types)
| 类型 | 说明 | 默认约束 | 存储映射 |
|---|---|---|---|
STRING | 字符串 | maxLength: 65535 | VARCHAR |
INTEGER | 32 位整数 | min: -2^31, max: 2^31-1 | INT |
LONG | 64 位整数 | min: -2^63, max: 2^63-1 | BIGINT |
DOUBLE | 64 位浮点 | 无 | DOUBLE |
DECIMAL | 精确小数 | precision: 38, scale: 10 | DECIMAL |
BOOLEAN | 布尔 | 无 | BOOLEAN |
DATE | 日期 | format: ISO-8601 | DATE |
TIMESTAMP | 时间戳 | timezone: UTC | TIMESTAMP |
TIME | 时间 | format: HH:mm:ss | TIME |
DURATION | 时间段 | format: ISO-8601 | VARCHAR |
# 标量类型示例
properties:
name:
type: STRING
required: true
constraints:
minLength: 1
maxLength: 200
price:
type: DECIMAL
constraints:
precision: 10
scale: 2
min: 0.00
createdAt:
type: TIMESTAMP
defaultValue: "$now" # 特殊变量:当前时间
isActive:
type: BOOLEAN
defaultValue: true
#2.2 枚举类型(Enum Type)
properties:
status:
type: ENUM
enumValues:
- value: DRAFT
displayName: "草稿"
description: "初始状态"
- value: ACTIVE
displayName: "激活"
description: "正在使用"
- value: DEPRECATED
displayName: "已弃用"
description: "计划下线"
- value: ARCHIVED
displayName: "已归档"
description: "不可用"
defaultValue: DRAFT
transitions: # 可选:状态机约束
DRAFT: [ACTIVE]
ACTIVE: [DEPRECATED]
DEPRECATED: [ARCHIVED]
枚举类型 vs STRING + 应用层校验:
STRING + 应用层: ENUM 类型:
┌───────────────────┐ ┌───────────────────┐
│ 数据库存 "actve" │ ← 拼写错误 │ 数据库存 "ACTIVE" │ ← Schema 验证
│ 数据库存 "Active" │ ← 大小写不一 │ 前端拿到枚举列表 │ ← 自动 UI
│ 前端硬编码选项列表 │ │ API 自动校验 │ ← 无需手写
│ 每个微服务重复校验 │ │ 状态机可选集成 │ ← 声明式
└───────────────────┘ └───────────────────┘
#2.3 复合类型(Composite Types)
properties:
# STRUCT:嵌套结构体
address:
type: STRUCT
structType: Address # 引用已注册的 StructType
# ARRAY:数组
tags:
type: ARRAY
itemType: STRING
constraints:
maxItems: 20
uniqueItems: true # 元素不可重复
# MAP:键值映射
metadata:
type: MAP
keyType: STRING
valueType: STRING
constraints:
maxEntries: 50
# REFERENCE:引用其他 ObjectType
ownerId:
type: REFERENCE
targetType: User
onDelete: SET_NULL # 级联策略
# ATTACHMENT:文件附件
documents:
type: ARRAY
itemType: ATTACHMENT
constraints:
maxFileSize: "10MB"
allowedMimeTypes:
- "application/pdf"
- "image/*"
#2.4 特殊类型(Special Types)
properties:
# GEO_POINT:地理坐标
location:
type: GEO_POINT
constraints:
srid: 4326 # WGS84 坐标系
# GEO_SHAPE:地理形状
boundary:
type: GEO_SHAPE
constraints:
allowedShapes: [POLYGON, MULTI_POLYGON]
# VECTOR:向量嵌入
embedding:
type: VECTOR
constraints:
dimensions: 768 # 向量维度
similarity: COSINE # 相似度计算方式
# TIMESERIES:时序数据引用
temperatureHistory:
type: TIMESERIES
valueType: DOUBLE
resolution: "1m" # 1 分钟分辨率
# EXPRESSION:表达式
displayLabel:
type: EXPRESSION
expression: "concat(name, ' (', code, ')')"
#2.5 类型完整关系图
Property Types
│
┌───────────────┼───────────────┐
│ │ │
Scalar Composite Special
│ │ │
┌─────────┼─────────┐ ┌─┼─────┐ ┌───┼─────┐
│ │ │ │ │ │ │ │ │ │ │
STRING INT LONG DOUBLE BOOL STRUCT ARRAY GEO VECTOR TIMESERIES
DATE TIMESTAMP DECIMAL MAP REFERENCE EXPRESSION
TIME DURATION ENUM ATTACHMENT
#3. 约束体系详解
#3.1 字段级约束(Field-Level Constraints)
properties:
email:
type: STRING
required: true # 非空约束
unique: true # 唯一约束
immutable: false # 创建后可修改
constraints:
pattern: "^[\\w.-]+@[\\w.-]+\\.\\w+$" # 正则约束
maxLength: 255
age:
type: INTEGER
constraints:
min: 0 # 最小值
max: 200 # 最大值
score:
type: DOUBLE
constraints:
min: 0.0
max: 100.0
multipleOf: 0.5 # 必须是 0.5 的倍数
startDate:
type: DATE
constraints:
futureOnly: true # 只允许未来日期
约束种类速查表:
| 约束 | 适用类型 | 说明 |
|---|---|---|
required | 所有 | 非空 |
unique | 所有标量 | 全局唯一 |
immutable | 所有 | 创建后不可变 |
defaultValue | 所有 | 默认值 |
min / max | 数值、日期 | 范围限制 |
minLength / maxLength | STRING | 长度限制 |
pattern | STRING | 正则匹配 |
multipleOf | 数值 | 倍数约束 |
futureOnly / pastOnly | DATE, TIMESTAMP | 时间方向 |
maxItems / minItems | ARRAY | 数组长度 |
uniqueItems | ARRAY | 元素唯一 |
maxEntries | MAP | 映射条目数 |
allowedMimeTypes | ATTACHMENT | 文件类型白名单 |
maxFileSize | ATTACHMENT | 文件大小限制 |
#3.2 对象级约束(Object-Level Constraints)
跨字段的业务规则:
spec:
properties:
startDate:
type: DATE
endDate:
type: DATE
minQuantity:
type: INTEGER
maxQuantity:
type: INTEGER
constraints:
# 跨字段约束
- name: dateRange
type: COMPARISON
expression: "startDate <= endDate"
message: "结束日期必须晚于开始日期"
- name: quantityRange
type: COMPARISON
expression: "minQuantity <= maxQuantity"
message: "最大数量必须大于等于最小数量"
# 条件必填
- name: conditionalRequired
type: CONDITIONAL
when: "status == 'ACTIVE'"
then:
required: ["activatedBy", "activatedAt"]
message: "激活状态下必须填写激活人和激活时间"
# 互斥约束
- name: mutualExclusive
type: MUTUAL_EXCLUSIVE
fields: ["internalCode", "externalCode"]
message: "内部编码和外部编码不能同时为空"
# 自定义规则
- name: luhnCheck
type: CUSTOM
expression: "luhn_check(cardNumber)"
message: "卡号校验位不正确"
#3.3 关系级约束(Relation-Level Constraints)
# 参照完整性
relations:
- name: OrderBelongsToCustomer
from: Order
to: Customer
cardinality: MANY_TO_ONE
constraints:
required: true # 每个 Order 必须有 Customer
onDelete: RESTRICT # 有关联 Order 时不能删 Customer
onUpdate: CASCADE # Customer 主键变更时级联更新
# 基数约束
- name: TeamHasMembers
from: Team
to: Employee
cardinality: ONE_TO_MANY
constraints:
minCount: 1 # 团队至少 1 人
maxCount: 50 # 团队最多 50 人
#4. 主键设计策略
#4.1 四种主键策略对比
┌──────────────┬─────────────┬────────────┬────────────┐
│ 策略 │ 示例 │ 优点 │ 缺点 │
├──────────────┼─────────────┼────────────┼────────────┤
│ 自然键 │ email │ 业务可读 │ 可能变更 │
│ Natural Key │ isbn │ 无需生成 │ 数据倾斜 │
├──────────────┼─────────────┼────────────┼────────────┤
│ UUID │ uuid-v4 │ 全局唯一 │ 索引膨胀 │
│ │ │ 无需协调 │ 不可读 │
├──────────────┼─────────────┼────────────┼────────────┤
│ 复合键 │ (tenant,id) │ 天然分区 │ 查询复杂 │
│ Composite │ │ 多租户友好 │ URL 不友好 │
├──────────────┼─────────────┼────────────┼────────────┤
│ 序列键 │ BIGINT auto │ 紧凑 │ 不可分布式 │
│ Sequence │ │ 有序 │ 信息泄露 │
└──────────────┴─────────────┴────────────┴────────────┘
#4.2 coomia-dip 推荐:语义化 ID 方案
spec:
primaryKey:
property: equipmentId
strategy: SEMANTIC_ID
config:
prefix: "EQ" # 类型前缀
separator: "-"
segments:
- type: PROPERTY
source: factoryCode # 来自工厂编码
length: 3
- type: SEQUENCE
length: 6 # 6 位序列号
# 生成结果:EQ-BJ1-000001, EQ-SH2-000001
决策矩阵:
分布式友好
低 ◄──────────────► 高
│ │
高 │ 序列键 │ UUID
性 │ │ (单机快) │ (分布式)
能 │ │ │
│ │ │
低 │ 自然键 │ 复合键
│ (需要稳定) │ (多租户)
│ │
└────────────────────┘
coomia-dip 推荐:语义化 ID(兼顾可读性和分布式)
#4.3 主键配置示例
from ontology_sdk import ObjectTypeSpec, PrimaryKeyConfig
# 策略 1:UUID(默认,最安全)
order = ObjectTypeSpec(
name="Order",
primary_key=PrimaryKeyConfig(
property="orderId",
strategy="UUID_V4"
)
)
# 策略 2:语义化 ID(推荐)
equipment = ObjectTypeSpec(
name="Equipment",
primary_key=PrimaryKeyConfig(
property="equipmentId",
strategy="SEMANTIC_ID",
prefix="EQ",
segments=[
{"type": "PROPERTY", "source": "factoryCode", "length": 3},
{"type": "SEQUENCE", "length": 6}
]
)
)
# 策略 3:复合键(多租户场景)
tenant_resource = ObjectTypeSpec(
name="TenantResource",
primary_key=PrimaryKeyConfig(
properties=["tenantId", "resourceId"],
strategy="COMPOSITE"
)
)
# 策略 4:自然键(稳定业务标识)
country = ObjectTypeSpec(
name="Country",
primary_key=PrimaryKeyConfig(
property="isoCode",
strategy="NATURAL",
immutable=True
)
)
#5. 属性的高级特性
#5.1 派生属性(Derived Properties)
properties:
# 表达式派生
fullName:
type: STRING
derived: true
expression: "concat(firstName, ' ', lastName)"
# 聚合派生(跨关系)
totalOrderAmount:
type: DECIMAL
derived: true
aggregation:
type: SUM
relation: CustomerPlacedOrder
property: amount
# 条件派生
riskLevel:
type: ENUM
derived: true
expression: |
CASE
WHEN creditScore >= 750 THEN 'LOW'
WHEN creditScore >= 600 THEN 'MEDIUM'
ELSE 'HIGH'
END
#5.2 审计属性(Audit Properties)
spec:
audit:
enabled: true
properties:
createdBy:
type: STRING
autoPopulate: "$currentUser"
createdAt:
type: TIMESTAMP
autoPopulate: "$now"
updatedBy:
type: STRING
autoPopulate: "$currentUser"
updatedAt:
type: TIMESTAMP
autoPopulate: "$now"
version:
type: INTEGER
autoIncrement: true # 乐观锁版本号
#5.3 敏感属性(Sensitive Properties)
properties:
ssn:
type: STRING
sensitive: true
masking:
strategy: PARTIAL # 部分掩码
visibleChars: 4 # 显示最后 4 位
maskChar: "*"
# 显示效果:***-**-1234
encryption:
algorithm: AES_256_GCM
keyId: "key-ssn-001"
access:
requiredPermission: "view:pii"
#6. 类型系统的运行时行为
#6.1 类型转换规则
┌─────────────────────────────────────────────────┐
│ 自动类型转换矩阵 │
├──────────┬──────────────────────────────────────┤
│ 源类型 │ 可安全转换到 │
├──────────┼──────────────────────────────────────┤
│ INTEGER │ LONG, DOUBLE, DECIMAL, STRING │
│ LONG │ DOUBLE, DECIMAL, STRING │
│ DOUBLE │ STRING │
│ DECIMAL │ STRING │
│ BOOLEAN │ STRING, INTEGER(0/1) │
│ DATE │ TIMESTAMP, STRING │
│ STRING │ (需显式解析,不自动转换) │
└──────────┴──────────────────────────────────────┘
安全转换 = 无信息丢失
不安全转换需要显式 CAST
#6.2 NULL 值处理
# coomia-dip NULL 语义
properties:
middleName:
type: STRING
required: false # 允许 NULL
nullSemantics: ABSENT # NULL = 值不存在(默认)
deletedAt:
type: TIMESTAMP
required: false
nullSemantics: NOT_APPLICABLE # NULL = 不适用(未删除)
score:
type: DOUBLE
required: false
nullSemantics: UNKNOWN # NULL = 值未知
defaultOnNull: 0.0 # 查询时 NULL 当作 0.0
#7. 实际案例:设计一个完整的 ObjectType
apiVersion: ontology/v1
kind: ObjectType
metadata:
name: Product
namespace: ecommerce
version: "1.0.0"
spec:
displayName: "商品"
description: "电商平台的商品实体"
primaryKey:
property: productId
strategy: SEMANTIC_ID
prefix: "PRD"
properties:
productId:
type: STRING
required: true
immutable: true
name:
type: STRING
required: true
constraints:
minLength: 1
maxLength: 500
searchable: true # 支持全文搜索
sku:
type: STRING
required: true
unique: true
constraints:
pattern: "^[A-Z]{2}-\\d{6}$"
category:
type: ENUM
enumValues: [ELECTRONICS, CLOTHING, FOOD, BOOKS, OTHER]
required: true
price:
type: DECIMAL
required: true
constraints:
precision: 10
scale: 2
min: 0.01
stock:
type: INTEGER
required: true
constraints:
min: 0
defaultValue: 0
images:
type: ARRAY
itemType: ATTACHMENT
constraints:
maxItems: 10
maxFileSize: "5MB"
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
specifications:
type: MAP
keyType: STRING
valueType: STRING
constraints:
maxEntries: 30
dimensions:
type: STRUCT
structType: PhysicalDimension
# 派生属性
avgRating:
type: DOUBLE
derived: true
aggregation:
type: AVG
relation: ProductHasReview
property: rating
totalSales:
type: INTEGER
derived: true
aggregation:
type: SUM
relation: ProductInOrderItem
property: quantity
isLowStock:
type: BOOLEAN
derived: true
expression: "stock < 10 AND stock > 0"
displayLabel:
type: EXPRESSION
expression: "concat(name, ' (', sku, ')')"
constraints:
- name: pricePositive
expression: "price > 0"
message: "商品价格必须大于零"
audit:
enabled: true
interfaces:
- Searchable
- Auditable
- SoftDeletable
lifecycle: DRAFT
#8. 属性类型选择决策树
需要存储什么类型的数据?
│
├── 单个值
│ ├── 文本 → STRING
│ ├── 整数 → INTEGER (< 2^31) 或 LONG
│ ├── 小数
│ │ ├── 需要精确计算(金额)→ DECIMAL
│ │ └── 近似即可(科学计算)→ DOUBLE
│ ├── 是/否 → BOOLEAN
│ ├── 时间
│ │ ├── 只有日期 → DATE
│ │ ├── 只有时间 → TIME
│ │ ├── 日期+时间 → TIMESTAMP
│ │ └── 时间段 → DURATION
│ ├── 有限选项 → ENUM
│ └── 引用其他实体 → REFERENCE
│
├── 结构化值
│ ├── 固定结构 → STRUCT
│ ├── 列表 → ARRAY
│ └── 键值对 → MAP
│
├── 特殊值
│ ├── 地理位置 → GEO_POINT / GEO_SHAPE
│ ├── 向量嵌入 → VECTOR
│ ├── 时序数据 → TIMESERIES
│ ├── 文件 → ATTACHMENT
│ └── 计算表达式 → EXPRESSION
│
└── 从其他属性计算得到 → 任意类型 + derived: true
#9. 性能考量
#9.1 索引策略
spec:
indexes:
# 单字段索引
- name: idx_product_sku
properties: [sku]
type: UNIQUE
# 复合索引
- name: idx_product_category_price
properties: [category, price]
type: BTREE
# 全文搜索索引
- name: idx_product_name_fts
properties: [name]
type: FULLTEXT
analyzer: "ik_smart" # 中文分词器
# 向量索引
- name: idx_product_embedding
properties: [embedding]
type: HNSW
config:
m: 16
efConstruction: 200
#9.2 存储优化建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 高基数 STRING | 考虑 maxLength | 避免 VARCHAR(MAX) |
| 频繁查询的 ENUM | 索引 + 物化 | 避免全表扫描 |
| 大 ARRAY | 考虑独立 ObjectType | 避免文档膨胀 |
| 大 MAP | 考虑独立 ObjectType | 避免键名爆炸 |
| ATTACHMENT | 分离存储 (MinIO) | 避免数据库膨胀 |
| TIMESERIES | 专用时序表 | 写入优化 |
#10. 常见错误与最佳实践
#错误 1:过度使用 STRING
# ❌ 错误
status:
type: STRING # 可以存入任何值
# ✅ 正确
status:
type: ENUM
enumValues: [ACTIVE, INACTIVE, SUSPENDED]
#错误 2:忽略精度需求
# ❌ 错误:金额用 DOUBLE,会有浮点精度问题
amount:
type: DOUBLE
# ✅ 正确:金额用 DECIMAL
amount:
type: DECIMAL
constraints:
precision: 10
scale: 2
#错误 3:嵌套过深
# ❌ 错误:3 层嵌套,查询困难
address:
type: STRUCT
structType: Address # Address 内又嵌套 City,City 内又嵌套 Country
# ✅ 正确:扁平化 + 引用
city:
type: REFERENCE
targetType: City
address:
type: STRUCT
structType: SimpleAddress # 只包含 street, zipCode
#错误 4:主键选择不当
# ❌ 错误:用邮箱做主键(可能变更)
primaryKey: email
# ❌ 错误:纯自增 ID(不支持分布式)
primaryKey:
strategy: SEQUENCE
# ✅ 正确:语义化 ID
primaryKey:
property: customerId
strategy: SEMANTIC_ID
prefix: "CUS"
#Key Takeaways
-
选择正确的属性类型是 Schema 设计的第一步。20+ 种类型覆盖了从简单标量到地理空间、向量嵌入的所有场景。优先使用最精确的类型(ENUM 而非 STRING,DECIMAL 而非 DOUBLE),让类型系统替你做验证。
-
约束是数据质量的第一道防线。三层约束体系(字段级→对象级→关系级)将业务规则下沉到 Schema 层,避免"应用层校验不一致"的经典问题。
-
主键设计影响深远。语义化 ID 方案兼顾了可读性、分布式友好和类型识别,是 coomia-dip 的推荐策略。避免用可变业务字段做主键。
#下一篇
S4-03: ObjectType 生命周期:从 DRAFT 到 ARCHIVED 的状态机 —— 我们将深入 ObjectType 的 4 个生命周期状态、转换规则和兼容性检查。
tags: object-type, property-types, constraints, primary-key, schema-design, coomia-dip, data-modeling, validation