protobufでMessageをmergeする


はじめに

protobufで複数のMessageをmergeする方法を紹介します。

merge処理自体は素朴にfieldを順番にコピーしても実現できますが、
いくつかmerge用のAPIも用意されています。

うまく活用できれば各Message型に依存しない形でmerge処理を記述することができたりします。
まとまった情報が見つからなかったので備忘のため記事にしました。

誤りやお気づきの点などありましたら、ぜひご指摘いただければ幸いです。

以下の環境で動作確認しています。
言語はC++で確認していますが、思想的には他の言語でも同じ結果になってくれると信じてます。

$ lsb_release -d
Description:    Ubuntu 18.04.5 LTS
$ uname -r
5.5.8
$ uname -m
x86_64
$ protoc --version
libprotoc 3.0.0

Message::MergeFrom

message_b.MergeFrom(message_a)とすることで、
message_aの中で値が設定されているfieldの値をmessage_bにコピーします。

message_amessage_bの両方に値が設定されているfieldがある場合は、message_aの値で上書きされます。
値が設定されているかどうかはdefaultと異なる値かどうかで判定されます。
例えば、bool fieldにfalseを設定したMessageをmergeしても、falseはdefault値なので上書きされません

repeated field (配列)の場合は、連結されます。

Messageの中にembedded message (Message内Message)がある場合、
そのembedded messageが同様のルールでmergeされます。

FieldMaskUtil::MergeMessageTo

Message::MergeFromはお手軽なぶん融通が効きません。
特にdefault値かどうかで上書きするfieldを決定するロジックは、直感に反するケースがあるため不具合につながりかねません。
そこでもう少し柔軟にmergeするために用意されているAPIがFieldMaskUtil::MergeMessageToです。

FieldMaskUtil::MergeMessageToMessage::MergeFromと比較して下記の2つの機能が追加されています。

  • FieldMaskによるmerge対象fieldの指定
  • FieldMaskUtil::MergeOptionsによるmergeロジックの指定

FieldMaskによるmerge対象fieldの指定

FieldMaskはその名の通り、処理対象のfieldを指定するためのmaskです。

下記のように、fieldを指すpathをFieldMaskに設定し、
そのFieldMaskを処理に渡すことで処理範囲を限定することができます。

FieldMask mask;
mask.add_paths("string_field");
mask.add_paths("bool_field");
mask.add_paths("repeated_int_field");
mask.add_paths("embedded_message_field.int_field");
FieldMaskUtil::MergeOptions merge_options;
FieldMaskUtil::MergeMessageTo(message_a, mask, merge_options, &message_b);

FieldMaskを指定することで不要な上書きを避けられるだけでなく、
仮にdefault値であったとしても上書きすることができます。

今回の例では"bool_field"FieldMaskに指定しているため、
bool型のdefault値であるfalseであっても上書きしています。

FieldMaskUtil::MergeOptionsによるmergeロジックの指定

FieldMaskUtil::MergeOptionsを使って2種類のオプションを設定できます。

  • MergeOptions::replace_message_fields
  • MergeOptions::replace_repeated_fields

MergeOptions::replace_message_fields

embedded message fieldの扱いを変更できます。

デフォルトではembedded message fieldはMessage::MergeFromと同じ挙動をします。
MergeOptions::replace_message_fieldsをtrueに設定することで、
値によらずmerge元のembedded message fieldにまるっと置き換えられるようになります。

FieldMask mask;
mask.add_paths("embedded_message_field");
FieldMaskUtil::MergeOptions merge_options;
merge_options.set_replace_message_field(true);
FieldMaskUtil::MergeMessageTo(message_a, mask, merge_options, &message_b);

MergeOptions::replace_repeated_fields

repeated fieldの扱いを変更できます。

デフォルトではrepeated fieldは連結されます。
MergeOptions::replace_repeated_fieldsをtrueに設定することで、
連結ではなくmerge元のrepeated fieldにまるっと置き換えられるようになります。

FieldMask mask;
mask.add_paths("repeated_int_field");
FieldMaskUtil::MergeOptions merge_options;
merge_options.set_replace_repeated_field(true);
FieldMaskUtil::MergeMessageTo(message_a, mask, merge_options, &message_b);

repeated embedded message fieldの各要素をmergeする

FieldMaskの説明には、repeated fieldについて以下の記載があります。

A repeated field is not allowed except at the last position of a field mask.

つまり、残念ながらrepeated fieldの各要素のmergeを指定する方法はないということになります。

もちろん各要素自体はMessage型なので、下記のように各要素ごとにmergeすることは可能ですが、
思いっきり具体Message型に依存した実装になります。
この依存をなくすためにはReflectionを使うしかないと思います。

assert(message_a.repeated_embedded_message_field_size() ==
       message_b.repeated_embedded_message_field_size());
int len = message_a.repeated_embedded_message_field_size();
for (int i = 0; i < len; i++) {
  FieldMaskUtil::MergeMessageTo(message_a.repeated_embedded_message_field(i), mask,
                                merge_options,
                                message_b.mutable_repeated_embedded_message_field(i));
}

実験コード

今回の記事で使った実験コードは、以下に置いています。

takeoverjp/protobuf-sandbox

参考

Message
FieldMaskUtil
FieldMask
C++ Generated Code