當你第一次定義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)。