当你第一次定义Protocol Buffer的消息的时候,你肯定会给消息设定一套规则需求。但是随着时间的推进,你的业务可能会发生了变化,与此同时,你的Protocol Buffer消息类型的需求也会随之变化。
也就是说:有一些字段可能会发生变化,可能会添加一些字段,也可能会删除一些字段。但是可能有很多程序正在使用/读取你的Protocol Buffer的消息,但是它们没法都随着需求进行更新。所以,在你对源数据进行演进的时候,一定不要引起破坏性变化,否则其它的程序可能就无法正常工作了。
主要有这两种情景:
向前兼容变更:使用新的.proto文件来写数据 --- 从旧的.proto文件读取数据
向后兼容变更:使用旧的.proto文件来写数据 --- 从新的.proto文件读取数据
有时候这两种情况同时存在,也就是全兼容变更。
为了达到此目的,Protocol Buffer制定了一些更新消息类型的规则:
不要修改任何现有字段的数字(tag)
你可以添加新的字段,那些使用旧的消息格式的代码仍然可以将消息序列化,您应该注意这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。类似的,新代码所创建的消息也可以被旧代码解析:旧的二进制在解析的时候会忽略新的字段。
字段可以被删除,只要它们的数字(tag)在更新后的消息类型中不再使用即可。你也可以把字段名改为使用“OBSOLETE_”前缀而不是删除字段,或者把这些字段的数字(tag)进行保留(reserved),以免未来其它开发者不消息使用了删除字段的数字。
对于数据类型的变化,例如int32到int64,string到bytes等等,可以参考官方文档:
https://developers.google.com/protocol-buffers/docs/proto3#updating。但是建议还是尽量不要去修改字段的数据类型。
添加字段
原来的proto是这样的:
然后我添加一个name字段:
而这时,如果把新的消息发送到旧的代码的时候,旧代码不知道2这个数字tag对应的是什么,所以name这个字段就会被忽略掉。
反过来,如果我们使用新的代码读取旧的数据,那么就会找不到新的字段,这时候就会使用该字段类型的默认值(空字符串)。
所以,处理默认值的时候一定要非常的小心。
对字段重命名
现在我把name这个字段的名改成了full_name,而它的数字不变:
这样做是没有任何问题的。
你可以随意改变字段的名字,只要它的数字tag不变就行,因为Protocol Buffer里面这个数字tag才是最重要的。
删除字段
现在我又把full_name字段删除了:
这时候,如果旧的代码找不到这个字段了,那么就会采用默认值。
反过来,如果我们使用新的代码读取旧的数据,那么已删除的字段将会被忽略/丢弃。
但是,在删除字段的时候,你应该一直都保留字段的数字tag以及字段名,像这样:
这样做是防止数字tag和名称被重复使用,避免在以后的代码库里造成冲突。
使用OBSOLETE
之前说了,可以把字段名改为 OBSOLETE_字段名 来代替删除字段,但是这样做的缺点就是:你还是需要把这个字段的值计算出来。我还是建议使用reserve的方式进行删除字段的管理。
Reserved
你可以保留字段的数字tag和字段名;
但是不可以在同一行语句里混合reserved数字tag和字段名,应该分成两个语句:
保留字段数字tag的目的就是防止数字tag被重复使用;
而保留字段名的目的就是防止出现一些程序bug;
注意:一定不要移除reserved的数字tags。
默认值
默认值在更新Protocol Buffer消息定义的时候有很重要的作用,它可以防止对现有代码/新代码造成破坏性影响。它们也可以保证字段永远不会有null值。
但是,默认值还是非常危险的:
你无法区分这个默认值到底是来自一个丢失的字段还是字段的实际值正好等于默认值。
那么应该怎么办?
需要保证这个默认值对于业务来说是一个毫无意义的值。例如 int32 pop(人口)默认值就可以设置为-1。
再就是,可能需要在你的代码里来做一些对默认值的判断,从而进行处理。
枚举
enum同样可以进化,就和消息的字段一样,可以添加、删除值,也可以保留值。
但是如果代码不知道它接收到的值对应哪个enum值,那么enum的默认值将会被采用。
例如这个enum:
如果程序代码接收到了5这个数值,那么它找不到对应的枚举值,所以就会使用这个枚举的默认值0(UNSPECIFIED)。