スキーマ展開

スキーマ変更のルール
ライター・スキーマとリーダー・スキーマ
スキーマ展開の機能

スキーマ展開とは、古いスキーマを使用してデータがストアに書き込まれた後にAvroスキーマが変更された場合のストアの動作を表す用語です。既存のスキーマを変更するには、フラット・テキスト・ファイルに格納されているスキーマを更新してから、ddl add-schemaコマンドを-evolveフラグ付きで実行して、新しいスキーマをストアに追加します。

たとえば、FullNameスキーマにミドル・ネーム・プロパティを追加する場合は、それをschema2.avscという名前のファイルに格納してから、ddl add-schemaコマンドを使用してストアに追加します。

スキーマを変更する際は、新規フィールドにデフォルト値を指定する必要があることに注意してください。これにより、古いスキーマを使用しているクライアントがその新しいフィールドを持たない値を新規作成した場合にエラーを回避できます。

{
   "type": "record",
   "namespace": "com.example",
   "name": "FullName",
   "fields": [
     { "name": "first", "type": "string" },
     { "name": "middle", "type": "string", "default": "" },
     { "name": "last", "type": "string" }
   ]
 }

次に示す変更は、スキーマに対して問題なく安全に実行できます。

これらの変更以外は安全でない場合があり、そのような変更は、実行するとスキーマをストアに追加する際にスキーマが拒否されるか、スキーマを使用しているクライアントのアップグレードの方法について考慮している場合にかぎり実行できるかのどちらかになります。このような問題は、ストアで現在有効なスキーマを変更(展開)しようとしたときに確認されます。詳細は、「スキーマの変更」を参照してください。

スキーマ変更のルール

ストアですでに使用されているスキーマを変更する場合は、次のルールについて注意する必要があります。

  1. 最善の結果を得るためには、スキーマのフィールドには常にデフォルト値を設定します。これにより、後で必要に応じてフィールドを削除できます。フィールドにデフォルト値を設定しないと、そのフィールドをスキーマから削除できません。

  2. フィールドのデータ型は変更できません。最初に作成したものとは異なるデータ型をフィールドに使用する必要がある場合は、適切なデータ型を使用する1つのフィールドをスキーマに新規追加します。

  3. スキーマにフィールドを追加する際は、そのフィールドにデフォルト値を設定する必要があります。

  4. 既存のフィールドの名前は変更できません。ただし、最初に作成したものとは異なる名前を使用してフィールドにアクセスする場合は、そのフィールドに別名を追加して使用します。

ライター・スキーマとリーダー・スキーマ

スキーマを変更すると、ストアでは複数のバージョンのスキーマが存在し、保持されることになります。値がストアに書き込まれる前の値のシリアライズに使用するスキーマのバージョンは、ライター・スキーマと呼ばれます。ライター・スキーマは、バインディングの作成時にアプリケーションによって指定されます。それは、バインディングのAvroBinding.toValue()メソッドを呼び出してデータをシリアライズする際に、値と関連付けられます。このライター・スキーマは、すべての格納済の値と内部的に関連付けられます。

リーダー・スキーマは、値がストアから読み取られた後の値のデシリアライズに使用します。ライター・スキーマと同様に、リーダー・スキーマは、バインディングの作成時にクライアント・アプリケーションによって指定されます。値がストアから読み取られた後に、バインディングのAvroBinding.toObject()メソッドを呼び出した際のデータのデシリアライズに使用します。

スキーマ展開の機能

スキーマ展開は、Avroスキーマの自動変換です。この変換は、クライアントが使用しているスキーマのバージョン(ローカル・コピー)とストアに現在格納されているバージョンとの間で行われます。スキーマのローカル・コピーが、値の書込みに使用するスキーマと同一でない(つまり、リーダー・スキーマがライター・スキーマと異なる)場合に、このデータ変換が実行されます。リーダー・スキーマが、値の書込みで使用するスキーマと一致する場合、変換は必要ありません。

スキーマ展開はデシリアライズ時にのみ適用されます。リーダー・スキーマが値のライター・スキーマと異なる場合、値はデシリアライズの際にリーダー・スキーマと一致するよう、自動的に変更されます。これにはデフォルト値が使用されます。

スキーマ展開を使用するにあたって、フィールドの追加時とフィールドの削除時の2つのケースについて検討する必要があります。スキーマ展開で両方のシナリオに対応できるのは、削除されたフィールドに最初からデフォルト値が割り当てられ、追加されたフィールドにデフォルト値を割り当てた場合にかぎります。

フィールドの追加

次のスキーマがあるとします。

{
    "type" : "record",
    "name" : "userInfo",
    "namespace" : "my.example",
    "fields" : [{"name" : "name", "type" : "string", "default" : ""}]
} 

スキーマのバージョン2で、次のようにフィールドを追加します。

{
    "type" : "record",
    "name" : "userInfo",
    "namespace" : "my.example",
    "fields" : [{"name" : "name", "type" : "string", "default" : ""},
                {"name" : "age", "type" : "int" , "default" : -1}]
                
} 

このシナリオでは、新しいスキーマを使用しているクライアントは、古いスキーマを使用している値にageフィールドがなくても、その値をデシリアライズできます。デシリアライズ時に、ストアから取得した値が自動的に変換され、その値にageフィールドが含まれるようになります。ageフィールドはデフォルト値(この場合は-1)に設定されます。

この反対も機能します。古いバージョンのスキーマを使用しているクライアントは、新しいバージョンのスキーマを使用して書き込まれた値をデシリアライズできます。この場合、ストアから取得した値には、クライアントにとっては想定外のageフィールドが含まれています。そのため、デシリアライズ時には、取得したオブジェクトからageフィールドが自動的に削除されます。

これは、異なるスキーマ・バージョンを使用している同時実行クライアントが存在する状態でスキーマを変更する場合に問題があります。このシナリオは、Oracle NoSQL Databaseがサポートしているような大規模な分散システムではめずらしくありません。

このシナリオでは、問題のフィールドを明示的に操作したクライアントがなくても、そのフィールドがデフォルト値に戻る場合があります。これは次のような状況で発生します。

  1. クライアントv.2がmy.example.userInfoレコードを作成してageフィールドを38に設定します。次に、その値をストアに書き込みます。クライアントv.2ではスキーマのバージョン2を使用しています。

  2. クライアントv.1がレコードを読み取ります。スキーマのバージョン1を使用しているので、デシリアライズ時に値からageフィールドが自動的に削除されます。

    クライアントv.1がnameフィールドを変更してから、ストアにそのレコードを書き込みます。これを行うと、ストアに書き込まれる値にはageフィールドがありません。

  3. クライアントv.2がレコードを再度読み取ります。レコードにはageフィールドがないので(クライアントv.1が最後に書込みを行ったため)、ageフィールドはデフォルト値(-1)に設定されています。つまり、ageフィールドを明示的に変更したクライアントがないのに、このフィールドの値はデフォルト値に戻っています。

フィールドの削除

フィールドの削除はフィールドの追加とほぼ同じように動作し、フィールドの値が自動的にデフォルト値に戻るという問題も同様です。次の簡単なスキーマがあるとします。

{
    "type" : "record",
    "name" : "userInfo",
    "namespace" : "my.example",
    "fields" : [{"name" : "name", "type" : "string", "default" : ""},
                {"name" : "age", "type" : "int" , "default" : -1}]
                
} 

スキーマのバージョン2で、次のようにageフィールドを削除します。

{
    "type" : "record",
    "name" : "userInfo",
    "namespace" : "my.example",
    "fields" : [{"name" : "name", "type" : "string", "default" : ""}]
} 

このシナリオでは、新しいスキーマを使用しているクライアントは、古いスキーマを使用している値にageフィールドが含まれていても、その値をデシリアライズできます。この場合、デシリアライズ時に、値からageフィールドが暗黙的に削除されます。

また、古いバージョンのスキーマを使用しているクライアントは、新しいバージョンのスキーマを使用している値をデシリアライズできます。この場合、ストアから取得した値にはageフィールドが含まれていません。そのため、デシリアライズ時には、スキーマにageフィールドが自動的に挿入され(リーダー・スキーマによって要求されるため)、新たに挿入されたフィールドにはデフォルト値が使用されます。

フィールドの追加と同様に、これは、異なるスキーマ・バージョンを使用している同時実行クライアントが存在する状態でスキーマを変更する場合に問題があります。

  1. クライアントv.1がmy.example.userInfoレコードを作成してageフィールドを38に設定します。次に、その値をストアに書き込みます。クライアントv.1ではスキーマのバージョン1を使用しています。

  2. クライアントv.2がレコードを読み取ります。スキーマのバージョン2が使用されているので、ageフィールドは想定外です。その結果、デシリアライズ時に、値からageフィールドが自動的に削除されます。

    クライアントv.2がnameフィールドを変更してから、ストアにそのレコードを書き込みます。これを行うと、ストアに書き込まれる値にはageフィールドがありません。

  3. クライアントv.1がレコードを再度読み取ります。レコードにはageフィールドがないので(クライアントv.2が最後に書込みを行ったため)、ageフィールドが値に自動的に挿入されます(デフォルトの-1を設定)。つまり、ageフィールドを明示的に変更したクライアントがないのに、このフィールドの値はデフォルト値に戻っています。