4. 数据契约定义语言(DCL):YAML/JSON格式定义契约、字段类型与约束声明、版本号与兼容性策略

数据契约不能只停留在口头约定上。你得把它写下来,而且要写得机器能读懂。

我个人习惯把这种描述语言叫做 DCL——Data Contract Language。说白了,它就是一份结构化的声明文件,告诉上下游:「我的数据长什么样,你得按这个规矩来」。

今天我们就聊聊怎么用 YAML 和 JSON 把契约写清楚。我还会分享一些踩过的坑,比如版本号没管好,差点让整个湖仓管道崩掉的故事。

4.1 为什么选 YAML 和 JSON?

你可能会问:为什么不用 XML?或者直接用数据库 DDL?

嗯,原因其实很简单。YAML 和 JSON 是数据工程师最熟悉的两种格式。它们轻量、可读性强,而且几乎所有编程语言都有现成的解析库。

我在项目中做过对比:

特性 YAML JSON XML
可读性 高(缩进结构) 中(括号多) 低(标签冗余)
注释支持 支持 不支持 支持
解析速度 中 快 慢
工具链成熟度 高 极高 高
适合场景 人工编写契约 程序生成/消费 遗留系统

我个人建议:契约源文件用 YAML,因为它支持注释,方便团队协作时写说明。程序内部流转或存储到元数据中心时,转成 JSON 更高效。

小技巧: 我习惯在 YAML 契约头部加一段元信息注释,写明作者、创建日期、变更原因。这样半年后回头看,不会一脸懵。

4.2 契约的基本结构

一份完整的数据契约,至少包含三部分:元信息、字段定义、约束声明。

来看一个实际例子。这是我为一个用户画像表写的契约:

# 数据契约:用户画像宽表
contract:
  name: "user_profile_v1"
  domain: "user"
  owner: "data-platform-team"
  version: "1.2.0"

schema:
  fields:
    - name: "user_id"
      type: "string"
      required: true
      description: "用户唯一标识"
      constraints:
        - pattern: "^U\\d{8}$"
        - max_length: 16

    - name: "user_name"
      type: "string"
      required: true
      description: "用户昵称"
      constraints:
        - max_length: 64

    - name: "age"
      type: "integer"
      required: false
      description: "用户年龄"
      constraints:
        - min: 0
        - max: 150

    - name: "registration_date"
      type: "date"
      required: true
      description: "注册日期"
      format: "yyyy-MM-dd"

    - name: "tags"
      type: "array"
      items:
        type: "string"
      required: false
      description: "用户标签列表"

compatibility:
  policy: "backward"
  breaking_changes:
    - "remove_field"
    - "rename_field"
    - "change_type"

你看,这份契约把字段名、类型、是否必填、约束条件都写清楚了。生产者按这个结构产出数据,消费者按这个结构解析数据,谁也别耍赖。

4.3 字段类型与约束声明

字段类型不能随便写。我见过有人把「整数」写成「string」,结果下游做聚合计算时全崩了。

我们一般支持以下几种基础类型:

  • string:字符串,可附加 max_length、pattern(正则)
  • integer:整数,可附加 min、max
  • float:浮点数,可附加 precision、scale
  • boolean:布尔值
  • date:日期,需指定 format
  • timestamp:时间戳,需指定 format 和 timezone
  • array:数组,需声明 items 的类型
  • map:键值对,需声明 key 和 value 的类型

核心原则: 约束越严格,数据质量越高,但灵活性越低。我建议在关键字段上收紧约束,在辅助字段上适当放宽。

约束声明这块,我曾经踩过一个坑。有个字段叫 "status",我定义成 string 类型,没加枚举约束。结果上游程序出了 bug,写入了 "sucess"、"faild" 这种拼写错误。下游消费端按 "success"、"failed" 做判断,直接跳过了 30% 的数据。

从那以后,我对枚举字段一律加 enum 约束:

constraints:
  - enum: ["active", "inactive", "suspended"]

4.4 版本号与兼容性策略

版本号是数据契约的命根子。没有版本管理,契约就是一纸空文。

我推荐使用 语义化版本:MAJOR.MINOR.PATCH。

版本位 变更类型 示例
MAJOR 不兼容变更 删除字段、修改字段类型
MINOR 向后兼容的新增 新增可选字段
PATCH 内部修正 修改描述、调整约束范围

兼容性策略有三种常见模式:

  • 向后兼容(Backward):新版本可以读取旧版本的数据。这是最常用的策略。
  • 向前兼容(Forward):旧版本可以读取新版本的数据。实现难度大,但适合长期运行的系统。
  • 完全兼容(Full):双向兼容。理想状态,但维护成本高。
注意: 不要轻易承诺「完全兼容」。我在一个跨团队项目中试过,结果每次变更都要协调 6 个团队,光开会就花了两周。后来改成「向后兼容为主,重大变更提前两个版本预告」,效率高多了。

4.5 知识体系总览

下面这张图把 DCL 的核心要素串起来了。你可以把它当作设计契约时的检查清单。

数据契约定义语言(DCL)核心结构 数据契约 元信息 name / domain / owner version / description created_at / updated_at 字段定义 name / type / required description / format default / nullable items(数组专用) 约束声明 min / max / pattern max_length / enum precision / scale unique / foreign_key 版本号 + 兼容性策略 语义化版本 MAJOR.MINOR.PATCH 1.2.0 → 2.0.0 兼容性策略 Backward Forward / Full 破坏性变更 remove_field rename_field / change_type

4.6 实战建议

最后,分享几个我在实战中总结的经验:

  1. 契约文件要纳入版本控制。和代码一起放在 Git 仓库里,每次变更都有记录。
  2. 自动化校验。CI/CD 流程里加一步契约校验,格式不对直接阻断发布。
  3. 版本号变更要通知所有消费者。我习惯在契约里加一个 consumers 字段,列出所有下游系统,变更时自动发通知。
  4. 不要过度设计。刚开始用 5-10 个字段就够了,别想着一次定义 100 个字段。迭代着来。

一句话总结: 数据契约不是写给人看的,是写给系统和团队之间的信任看的。格式清晰、版本可控、约束明确,这三点做到位,湖仓一致性就有了最底层的基础。