📊 第5章 大数据的收集
📄 一句话概括

上游把 amt 改名成 amount,说「兼容升级」——旧代码直接 KeyError。 「字段没删」不等于「兼容」。改名是最常见的破坏性变更。 最隐蔽的是改语义:名字没变、类型没变,但「含税」变成了「不含税」。

📝 本章和前面几章的关系

这一章最后一篇,讲一个所有数据管道都会遇到的事:格式变了。 教材 5.10 节:应对数据结构的变化——数据结构会跟着业务的发展而变化。 这一篇的结论会直接引出第 8 章:要管住结构,就得靠元数据。

一、上游改一版,下游全崩

演示里 v1 的数据长这样:{"id": 1, "amt": 100.0}。 v2 变成:{"id": 1, "amount": 100.0, "currency": "CNY", "ver": 2}。 两处变化:字段改名(amt 变 amount),多了两个字段。

上游说这是「兼容升级」,理由是旧字段没删——但旧代码一行都跑不了。 sum(r["amt"] for r in v2) 直接抛 KeyError: 'amt'。 「字段没删」不等于「兼容」。 改字段名在类型检查里看不出来,在测试里也不一定碰得到, 上线才发现。

二、兼容写法:新字段优先,读不到退回老的

最小的兼容做法是 r.get("amount", r.get("amt", 0.0))。 v1 和 v2 都算出 350.00,代码不用改就能同时读两版。 但这里有个坑:如果字段是「真的缺了」而不是「改名了」,这个写法会静默地补 0——数字错了,程序不报错。

静默错误比崩溃更可怕。 崩了你知道要修,静默错了可能三个月后才被发现。 所以更稳的做法是:缺值时记一条日志,或者抛一个明确的异常,让问题在第一时间暴露。 宽容读、严格报——这两个不矛盾。读的时候能兼容就兼容,但把「用了兼容路径」这件事记下来。

三、三种应对方式

加版本号:数据里带 ver,按版本走不同的解析逻辑。好处是明确,坏处是代码要维护多套。 只加不改:新字段只增,老字段永远留着。 好处是简单,坏处是几年之后一张表三百个字段,一半没人用也不知道该不该删。 加一层映射:中间放一个 schema 转换层,上游怎么变都在这一层消化。 好处是下游干净,坏处是多一个组件要维护,而且它自己会变成瓶颈。

教材 5.10 节讲的就是这三条。没有哪条最好,只有哪条你能维护。 实践中最常见的是第二条:只加不改。 它最省事,但一定要配一个「字段清单」,否则半年后没人知道哪些字段还活着。

四、五种变化,只有一种不破坏

加一个新字段:不破坏。老代码忽略它就行。 字段改名:破坏。老代码读不到。 字段类型变:破坏。金额从数字变字符串,q * a 直接报错。

删字段:破坏。 改语义:最隐蔽。 名字没变、类型没变,但「含税」变成了「不含税」, 或者是「下单时间」变成了「支付时间」。 前四种至少会报错,第五种连错都不报——报表出来数字全对,但意思全变了。

这种只能靠两件事防:写清楚的定义,以及能查到的历史。 这就是第 8 章「元数据管理」要解决的问题。 所以这一章到这里收尾,下一章换个角度:不是「怎么把数据弄进来」,而是「怎么管住它长什么样」。

五、动手 Demo:字段改个名,旧代码就跑不了了

造两版数据:v1 用 amt,v2 改成 amount 并多了两个字段。 先让旧代码原样跑,看它怎么崩。 再用兼容写法读两版,比较结果。

v1 = [{'id': 1, 'amt': 100.0}, {'id': 2, 'amt': 250.0}]
v2 = [{'id': 1, 'amount': 100.0, 'currency': 'CNY', 'ver': 2},
      {'id': 2, 'amount': 250.0, 'currency': 'CNY', 'ver': 2}]

# 旧代码:直接取 amt
total = sum(r['amt'] for r in v2)        # KeyError: 'amt'

# 兼容写法:新字段优先,读不到退回老的
total = sum(r.get('amount', r.get('amt', 0.0)) for r in v2)

实跑输出:

--------------------------------------------------------------------
数据长出新字段,下游代码就崩了
--------------------------------------------------------------------

  ① 上游改了一版
  --------------------------------------------------------------------
  v1:{'id': 1, 'amt': 100.0}
  v2:{'id': 1, 'amount': 100.0, 'currency': 'CNY', 'ver': 2}
  两处变化:amt 改名成 amount,还多了 currency 和 ver 两个字段。
  上游说「这是兼容升级」,因为旧字段没删——但旧代码读不了。

  ② 旧代码原样跑,直接崩
  --------------------------------------------------------------------
  KeyError: 'amt'
  能不能算出来:不能
  「字段没删」不等于「兼容」。 改字段名是最常见的破坏性变更,因为它在类型检查里看不出来。

  ③ 兼容的写法:给默认值
  --------------------------------------------------------------------
  用 r.get('amount', r.get('amt', 0)) 读:
    v1 算出来 350.00
    v2 算出来 350.00
  两边一样。这就是「向后兼容」的最小做法:新字段优先,读不到就退回老字段。
  但要注意:如果字段是「真的缺了」而不是「改名了」,这个写法会静默地补 0,
  结果是数字错了但程序不报错。静默错误比崩溃更可怕。

  ④ 三种应对方式
  --------------------------------------------------------------------

| 做法 | 怎么做 | 代价 |
|---|---|---|
| 加版本号 | 数据里带 ver,按版本走不同解析 | 代码要维护多套 |
| 只加不改 | 新字段只增不改名,老字段永远留着 | 字段会越攒越多 |
| 加一层映射 | 中间放一层 schema 转换 | 多一个组件要维护 |

  教材 5.10 节讲的「应对数据结构的变化」,落到工程上就是这三条。没有哪条最好,只有哪条你能维护。
  最常见的是第二条:只加不改。
  它最省事,但几年之后一张表能有三百个字段,一半没人用还不知道该不该删。

  ⑤ 所以结构要管起来
  --------------------------------------------------------------------

| 变化类型 | 破坏性吗 | 例子 |
|---|---|---|
| 加一个新字段 | 不破坏 | 多加一个 currency |
| 字段改名 | 破坏 | amt 改成 amount |
| 字段类型变 | 破坏 | 金额从数字变字符串 |
| 删字段 | 破坏 | 删掉 amt |
| 改语义 | 最隐蔽 | amt 从「含税」变成「不含税」 |

  最后一行最难防:字段名没变、类型没变,但含义变了。
  这种只能靠文档和元数据管理——第 8 章整章讲的就是这个。

第一段那两个字典摆在一起,差别一眼能看出来。但真实系统的字段有几十个,改名藏在里面根本看不见。 第二段那个 KeyError 是本篇的核心证据:上游什么都没删,下游照样崩。 第三段两版都算出 350.00。兼容写法很好用,但它有个副作用——真缺值的时候会静默补 0。 最后那张五种变化的表建议存下来。做接口评审的时候照着对一遍,能提前发现大部分问题。

30 秒实验

  1. 把兼容写法的默认值从 0.0 改成 None, 看 sum 会报什么错——这就是「静默错误」变成「显式错误」的做法。
  2. 给 v2 加一个 ver 字段,写一段按版本分派的代码,看要多几行。
  3. 想想你自己的项目里,有没有「字段名没变但含义变了」的情况。

常见误区

  1. 「我只是加了个字段,肯定兼容。 」加字段一般不破坏,但改名和改类型会。而且很多人把改名也叫做「加字段」。
  2. 「用 get 带默认值就万无一失了。」会静默补默认值,数字错了不报错。要记日志。
  3. 「结构变了让下游改一下就行。」下游可能有十个系统,而且有的已经没人维护了。
自测三问(点开对答案) 1. 为什么「字段没删」不等于「兼容」? —— 因为改名字会让旧代码取不到值。字段还在,但键不一样了,对旧代码来说等于不存在。 2. `r.get('amount', r.get('amt', 0.0))` 有什么风险? —— 如果字段是真的缺失(不是改名),它会静默补 0,导致数字错但不报错。要配合日志或明确异常。 3. 五种结构变化里,哪种最难防? —— 改语义。名字、类型都没变,程序不会报错,但算出来的数含义变了。只能靠文档和元数据管理。

下一篇进第 6 章:数据攒下来之后怎么存——数据池、仓库、列式存储。

相关笔记