.protoファイルのアップデートにおける注意点まとめ(protobuf)
Protocol Buffers には様々な用途があるようですが、個人的には gRPC での利用を考えています。 昨年末には Go や Dart/Flutter で gRPC を扱う方法を調べ、使っていけそうだと感じました。
そのときに記事にまとめて Qiita へ投稿しましたので、興味のある方はご覧ください。
使い方を概ね把握しても、プロトコル定義ファイル(.proto)で一度定義した message を将来変えたくなったときにどうするのかを知らないまま本格的に使い始めるのは不安があります。 前もってアップデート時の注意点を把握し、備忘のためにメモしておくことにしました。
免責
次のことを理解した上でご覧ください。
- 本記事は、忘れてもさっと復習できるようにまとめているだけです。網羅できていない可能性もあります。
- 公式ドキュメント(主に proto3 の Language Guide) を基にしていますが、その更新を常に反映していくとは限りません。
用語
気をつけるポイント
既存のフィールド
新たなフィールド
- フィールドを追加すれば message の形式が変わるが、古いコードでシリアライズされた message は新たなコードでパースできる
- 同様に、逆(新しいコードで生成された message を古いコードでパース)も可能
- 新たに追加されたフィールドは
Unknown field
(付録参照)- 古いバイナリではパース時に単に無視されるので、既存コードへの影響を気にする必要はない
- デフォルト値を考慮
- 新しいコードでも古いコードで生成された message と適切にやり取りできるよう、要素のデフォルト値について考慮すること(付録を参照のこと)
フィールドの削除
- 削除したフィールドの番号が再利用されない限り OK
- message をバイナリ形式にしたときにフィールドを特定するための「ユニークな」番号
- ユニークでなければならないのに再利用してしまえば当然問題が生じる
- 番号の再利用を防ぐ方法
- 再利用を防ぎたい番号や範囲を
reserved
で指定する- 使おうとしてしまったときに警告が出て気づける
- 番号だけでなく名前で指定することもできるが、番号と名前を混ぜて指定することはできない
-
max
というキーワードで番号の最大値までを指定することもできる
- 削除する代わりにフィールドをリネーム(
OBSOLETE_
という接頭辞をつける等)する
- 再利用を防ぎたい番号や範囲を
- 削除の前には
[deprecated=true]
の オプション を使うと良いかもしれない
reserved 2, 15, 9 to 11, 40 to max; reserved "FOO", "BAR";
将来を考えた附番
- サイズを意識
- エンコードすると
1
~15
は 1 バイト、16
~2047
は 2 バイトになる(番号と型を含めて) - 頻繁に使う message 要素には 1 ~ 15 を使うのが良い
- 使用頻度の高い要素が将来追加されたときのために 1 ~ 15 の範囲にいくらか空きを残しておくこと
- エンコードすると
int32 / uint32 / int64 / uint64 / bool
- どれも互換
- 前方/後方互換性を壊さずに相互に入れ替えることが可能
- ただし、ビット数が足りなければ切り詰められる(64 ビットの数が int32 として読まれたときなど)
sint32 と sint64
- 互換だが、他の整数型とは非互換
string と bytes
- bytes が有効な UTF-8 である限り互換
埋め込まれた message
- message のエンコードされたバージョンが bytes に含まれていればその message と bytes は互換
fixed32 と sfixed32
- 互換
fixed64 と sfixed64
- 互換
enum
- int32 / uint32 / int64 / uint64 と互換だが、サイズが合わなければ切り詰められる
oneof
複数のフィールドがあってもそのうちの一度に一つしかセットすることがない場合、oneof を使うとメモリを節約できる。 しかし、アップデートにおいては注意が必要な点が多い。
- フィールドを
oneof
に追加したりoneof
の外に移動したりする場合- message がシリアライズされてパースされると情報が一部失われることがある(一部のフィールドがクリアされる)
- 既存の
oneof
に入れるのは安全でない
- 既存の
- ただし、一つのフィールドを新たな
oneof
に移すのは安全 - 一つしかセットされないのがわかっているなら複数フィールドを移すのも安全
- message がシリアライズされてパースされると情報が一部失われることがある(一部のフィールドがクリアされる)
oneof
のフィールドを一つ消して追加し直す場合- message がシリアライズされてパースされると、セットされている
oneof
フィールドがクリアされることがある
- message がシリアライズされてパースされると、セットされている
oneof
を分離/統合した場合- 通常のフィールドを移動したときと同様の問題が起こる
map
map<key_type, value_type> map_field = N;
これは wire
フォーマット上では下記に相当する。
message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
map
をサポートしていない実装でも扱えるので、そういった言語にも対応させられるかという心配は無用。
.proto ファイルの変更
定義の記述を別ファイルに移す(下記例では old.proto から new.proto へ)とき、元のファイル(old.proto)で import public
を使って新たなファイル(new.proto)を読み込めば
クライアントでは読み込み対象を変えずに済む。
new.proto
// old.proto に書かれた定義をすべてこのファイルに移動
old.proto
// どのクライアントもこのファイルをインポートしている import public "new.proto"; import "other.proto";
client.proto
import "old.proto"; // old.proto と new.proto で定義したものは使えるが、other.proto の定義は使えない
なお、protoc
コマンドでコンパイルするとき、-I
や --proto_path
で指定したディレクトリがインポートファイルの検索対象になる(指定しないと実行時のカレントディレクトリ)。
それを使ってプロジェクトのルートを指定し、インポートで完全修飾名を使うのが良い。
付録
デフォルト値
- string
- 空文字列
- bytes
- 空バイト列
- bool
- false
- 数値型
- 0
- enum
- 定義されている最初の値(= 0)
- message フィールド
- セットされない(言語依存)
- 詳細は generated code guide を参照のこと
- repeated フィールド
- 空(各言語の空リスト)
- スカラー型のフィールド
- パースされると、フィールドにデフォルト値が入れられていたのか何も入っていなかったのか区別できないことに留意しておくこと
- 例えば bool 型の場合、false のときの振る舞いがデフォルトの振る舞いにもなる
- それが困るなら bool 型は避けること
- パースされると、フィールドにデフォルト値が入れられていたのか何も入っていなかったのか区別できないことに留意しておくこと
proto2 と proto3
- proto1 は無い
- 今は proto3
- proto2 と完全互換ではない
- proto2 もサポートは続けられるが、使いやすくて対応言語も多い proto3 のほうが良い
- proto3 で proto2 の message 型を使うこともその逆も可能
- ただし proto2 の
enum
を proto3 で直接使うことはできない- proto2 の message で使い、それを proto3 でインポートするのは OK
- ただし proto2 の
syntax
の指定を省略すると proto2 とみなされる
syntax = "proto3";
proto3 での変更点
required
とoptional
は廃止repeated
のpacked
がデフォルトでtrue
なので指定不要- proto2 ではエンコーディングを効率的に行うために
[packed=true]
のオプションが必要だった
- proto2 ではエンコーディングを効率的に行うために
enum
の先頭要素は0
- 先頭要素でなければならないのは proto2 との互換性のため(proto2では最初の要素が常にデフォルト)
Unknown fields
の扱いが v3.5 で変わった
上記は一部です。 全体的な変更点は次の記事が参考になると思います。
*1:gRPCのシリアライゼーション形式をJSONにする - Qiita
*2:ドキュメントには Java の @Deprecated のことしか書かれていないので他の言語については不明・未確認。