汎用バインディング

1つの汎用スキーマ・バインディングの使用
複数の汎用スキーマ・バインディングの使用
埋込みレコードの使用
汎用スキーマの動的管理

汎用バインディングは、Avroデータ型を広範囲にサポートしています。また、アプリケーションがストアで使用されているスキーマのセット全体をコンパイル時に認識している必要がないという点で柔軟です。これにより、ストアのスキーマ・セットが頻繁に拡張される環境で優れた柔軟性を実現できます。

汎用バインディングのマイナス面は、コンパイル時の型の安全性を備えていないことです。汎用バインディングでは、(固有バインディングが提供しているgetterメソッドとsetterメソッドとは異なり)文字列を使用してフィールドを識別するので、コンパイラは、たとえば浮動小数点数を想定しているところに整数が使用されているかどうかなどをコンパイル時に認識できません。

汎用バインディングでは、1つのスキーマのバインディングにはAvroCatalog.getGenericBinding()を、複数のスキーマを使用する場合はAvroCatalog.getGenericMultiBinding()を使用します。

1つの汎用スキーマ・バインディングの使用

{
    "type": "record",
    "name": "PersonInformation",
    "namespace": "avro",
    "fields": {"name": "ID", "type": "int"}
} 

ここで、スキーマをPersonSchema.avscという名前のファイルに設定したとします。

そのスキーマを使用するには、まず、ddl add-schemaコマンドを使用してストアに追加する必要があります。

> java -jar <kvhome>/kvstore.jar runadmin -port <port> \
-host <host>
kv-> ddl add-schema -file PersonSchema.avsc 

Oracle NoSQL Databaseクライアント・コードで、コードからそのスキーマを使用できるようにする必要があります。これを行う1つの方法は、スキーマの作成先のファイルからそのスキーマを直接読み取ることです。

package avro;

import java.io.File;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.Schema;

import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.GenericAvroBinding;

...

final Schema.Parser parser = new Schema.Parser();
parser.parse(new File("PersonSchema.avsc")); 

次に、アプリケーションからそのスキーマを使用できるようにする必要があります。

final Schema personSchema = 
    parser.getTypes().get("avro.PersonInformation"); 

最後に、バインディングを作成し、そのバインディングに対してAvroレコードを作成すれば、Avroデータ形式を使用する値のシリアライズとデシリアライズを開始できるようになります。この例では、汎用バインディングを使用します。ただし、この章の後半で説明するように、バインディングは他にもあるので、目的の用途に汎用バインディングが最適であるとはかぎりません。

/**
 * Here, for the sake of brevity, we skip the necessary steps of 
 * declaring and opening the store handle.
 */
final AvroCatalog catalog = store.getAvroCatalog();
final GenericAvroBinding binding = 
    catalog.getGenericBinding(personSchema);

バインディングが作成されると、アプリケーションには、スキーマのフィールドを読み書きできるような表現方法が必要になります。それには、Avroレコード(スキーマのフィールドの読取りと書込みを可能にするデータ構造)を作成します。(データが含まれているバイナリ・オブジェクトに対するハンドルであるAvroレコードと、ストアに含まれているキーと値の1ペアであるOracle NoSQL Databaseレコードを混同しないでください。Oracle NoSQL Databaseレコードには、Avroデータ形式を使用する値を含めることができます。一方、Avroデータ形式のインスタンスは、Avroレコードを使用してクライアント・コード内で管理されます。)

この例では汎用バインディングを使用しているので、GenericRecordを使用してバインディングの内容を管理します。

たとえば、ストアの読取りを実行した後で、Oracle NoSQL Databaseレコードに格納されている情報を調べようとしていると仮定します。

/**
  * Assume a store read was performed here, and resulted in a 
  * ValueVersion instance called 'vv'. Then, to deserialize
  * the value in the returned record:
  */
final GenericRecord member;
final int ID;
if (vv != null) {
    /* Deserialize the the value */
    member = binding.toObject(vv.getValue());
    /* Retrieve the contents of the ID field. Because we are 
     * using a generic binding, we must type cast the retrieved
     * value.
     */
     ID = (Integer) member.get("ID");
} 

フィールドに書き込む(つまり、一部のデータをシリアライズする)場合は、レコードのput()メソッドを使用します。例として、まったく新しいAvroオブジェクトを作成し、ストアに書き込むとします。その場合は、次のようにします。

final GenericRecord person = new GenericData.Record(personSchema);
final int ID = 100011;
person.put("ID", ID); 

/**
  * To serialize this information so that it can be written to 
  * the store, use GenericBinding.toValue() as the value for the
  * store put(). That is, assuming you already have a store handle 
  * and a key:
  */
store.put(key, binding.toValue(person)); 

複数の汎用スキーマ・バインディングの使用

アプリケーションで使用するスキーマが1つのみという状況は考えにくいです。複数のスキーマを使用するためには、次の手順を実行します。

  1. 各スキーマをそれぞれ別のファイルに指定します。

  2. これらのスキーマをすべてストアに追加します(「ストアのAvroスキーマの管理」で説明)。

  3. バインディングを作成するには、HashMapを使用してスキーマを編成してから、それをAvroCatalog.getGenericMultiBinding()に渡します。

たとえば、次の2つのスキーマがあるとします。

{
 "type": "record",
 "namespace": "avro",
 "name": "PersonInfo",
 "fields": [
   { "name": "first", "type": "string" },
   { "name": "last", "type": "string" },
   { "name": "age", "type": "int" }
 ]
}


{
 "type": "record",
 "namespace": "avro",
 "name": "AnimalInfo",
 "fields": [
   { "name": "species", "type": "string"},
   { "name": "name", "type": "string"},
   { "name": "age", "type": "int"}
 ]
} 

その場合は、1つのファイル(PersonSchema.avsc)にAvro.PersonInfoを、2つ目のファイル(AnimalSchema.avsc)にAvro.AnimalInfoをそれぞれ格納します。コマンドライン・インタフェースを使用して、これらのスキーマをストアに追加します。

これで、使用しているスキーマごとに1つのバインディングを簡単に作成できましたが、コードで使用しているスキーマが多ければ、この方法はすぐに不便に感じられるはずです。かわりに、HashMapと(この場合は) AvroCatalog.getGenericMultiBinding()を使用して、複数のスキーマを作成します。これを行うには、まず、スキーマの編成に使用するHashMapを作成します。

package avro;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;

...

import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.GenericAvroBinding;

...

HashMap<String, Schema> schemas = new HashMap<String, Schema>(); 

次に、各スキーマを解析して、HashMapに追加します。

final Schema.Parser parser = new Schema.Parser();

Schema personSchema = parser.parse(new File("PersonSchema.avsc"));
schemas.put(personSchema.getFullName(), personSchema);

Schema animalSchema = parser.parse(new File("AnimalSchema.avsc"));
schemas.put(animalSchema.getFullName(), animalSchema);

その後で、バインディングを作成します。複数のスキーマの使用に対応した複数バインディングを使用するので、必要なバインディングは1つのみです。

/*
 * Store creation is skipped for brevity
 */

catalog = store.getAvroCatalog();
binding = catalog.getGenericMultiBinding(schemas); 

このバインディングを使用するには、通常の単一スキーマ・バインディングを使用している場合と同じように、toObject()またはput()を呼び出します。複数バインディングでは、使用するスキーマを判断し、それに応じてシリアライズまたはデシリアライズできます。たとえば、Avro.AnimalInfoスキーマを使用するレコードを取得するとします。その場合、単一スキーマ・バインディングを使用しているかのように、デシリアライズできます。

/*
 * Key creation and store retrieval skipped.
 * Assume we have retrieved a ValueVersion (vv1) that
 * contains an AnimalInfo value.
 */

final GenericRecord animalObject;
if (vv1 != null) {
    animalObject = binding.toObject(vv1.getValue());
    final String species = animalObject.get("species").toString();
    final String name = animalObject.get("name").toString();
    final int age = (Integer) animalObject.get("age");

    /* Do something with the data */
} 

また、次に示すように、同じバインディングを使用して新しいAvro.PersonInfoオブジェクトを作成し、ストアに配置することもできます。

final GenericRecord personObject = 
    new GenericData.Record(personSchema);
personObject.put("name", "Sam Brown");
personObject.put("age", 34);

/*
 * Key creation and store handle creation skipped
 * for brevity's sake.
 */

store.put(aKey, binding.toValue(personObject)); 

埋込みレコードの使用

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

{
    "type" : "record",
    "name" : "hatInventory",
    "namespace" : "avro",
    "fields" : [{"name" : "sku", "type" : "string", "default" : ""},
                  {"name" : "description",
                     "type" : {
                         "type" : "record",
                         "name" : "hatInfo",
                         "fields" : [
                                     {"name" : "style", 
                                      "type" : "string", 
                                      "default" : ""},

                                     {"name" : "size", 
                                      "type" : "string", 
                                      "default" : ""},

                                     {"name" : "color", 
                                      "type" : "string", 
                                      "default" : ""},

                                     {"name" : "material", 
                                      "type" : "string", 
                                      "default" : ""}
                            ]}
                }
    ]
}

埋込みレコードhatInfoのフィールドを処理するには、スタンドアロン・スキーマの2番目の部分として扱います。スキーマ・ファイルは一度だけ解析する必要があります。その後で、スキーマを2つ、レコードを2つ、バインディングは1つのみ作成します。たとえば、このスキーマを使用するシリアライズされたオブジェクトを作成するには、次のようにします。

package avro;

import java.io.File;

import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.Schema;

import oracle.kv.KVStore;
import oracle.kv.Key;
import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.GenericAvroBinding;

...

// Parse our schema file
final Schema.Parser parser = new Schema.Parser();
try {
    parser.parse(new File("HatSchema.avsc"));
} catch (IOException io) {
    io.printStackTrace();
}


// Get two Schema objects. We need two because of the 
// embedded record.
final Schema hatInventorySchema =
    parser.getTypes().get("avro.hatInventory");
final Schema hatInfoSchema =
    parser.getTypes().get("avro.hatInfo");

// Get two GenericRecords so we can manipulate both of
// the records in the schema
final GenericRecord hatRecord = 
    new GenericData.Record(hatInventorySchema);
final GenericRecord hatInfoRecord = 
    new GenericData.Record(hatInfoSchema);

// Now populate our records. Start with the 
// embedded record.
hatInfoRecord.put("style", "western");
hatInfoRecord.put("size", "medium");
hatInfoRecord.put("color", "black");
hatInfoRecord.put("material", "leather");

// Now the top-level record. Notice that we
// set the embedded record as the value for the 
// description field.
hatRecord.put("sku", "289163009");
hatRecord.put("description", hatInfoRecord);

// Now we need a binding. Only one is required,
// and we use the top-level schema to create it.
final AvroCatalog catalog = store.getAvroCatalog();
final GenericAvroBinding hatBinding =
    catalog.getGenericBinding(hatInventorySchema);

// Create a Key and write the value to the store.
final Key key = Key.createKey(Arrays.asList("hats", "0000000033"));
store.put(key, hatBinding.toValue(hatRecord)); 

取得時には、次の方法でこれらの値を編集します。

// Perform the retrieval
final ValueVersion vv = store.get(key);
if (vv != null) {
    // Deserialize the ValueVersion as normal
    GenericRecord hatR =      
        new GenericData.Record(hatInventorySchema);
    hatR = hatBinding.toObject(vv.getValue());

    // To access the embedded record, create a GenericRecord
    // using the embedded record's schema. Then get the
    // embedded record from the field on the top-level
    // schema that contains it.
    GenericRecord hatInfoR =
        new GenericData.Record(hatInfoSchema);
    hatInfoR = (GenericRecord) hatR.get("description");

    // Finally, you can write to the top-level record and the
    // embedded record like this:

    // Modify a field on the embedded record:
    hatInfoR.put("style", "Fedora");

    // Modify the top-level record:
    hatR.put("sku", "300");
    hatR.put("description", hatInfoR);

    store.put(key, hatBinding.toValue(hatR)); } 

汎用スキーマの動的管理

汎用バインディングの特殊な使用例として、ユーザーがコードを記述する際にストアで今後使用されるスキーマをすべて把握しているとはかぎらない場合があげられます。たとえば、前述の例でHashMapを使用していますが、スキーマ・リストが頻繁に拡張される環境で操作している場合、これには多少の問題があります。このシナリオでは、ストアで使用されているスキーマに追加するたびに、クライアント・コードをリライトして、クライアントが使用しているHashMapに新しいスキーマを追加する必要が生じる可能性があります。そうしないと、コードでは、不明なスキーマを使用している値を取得する場合があります。コードの処理内容によっては、これが問題を引き起こすことがあります。

それが懸念される場合は、AvroCatalog.getCurrentSchemas()AvroCatalog.getGenericMultiBinding()を組み合せて使用することで問題を回避できるので、すべてのスキーマを網羅したHashMapを作成する必要はありません。

たとえば、前述の例では、既知のスキーマを2つ使用したクライアント・コードを示しました。それを、次のようにして、getCurrentSchemas()を使用するように変更できます。

package avro;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;

...

import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.GenericAvroBinding;

...

final Schema.Parser parser = new Schema.Parser();
Schema animalSchema = parser.parse(new File("AnimalSchema.avsc"));


/*
 * We skip creating a HashMap of Schemas because
 * it is not needed.
 */


/*
 * Store creation is skipped for brevity
 */

catalog = store.getAvroCatalog();
binding = catalog.getGenericMultiBinding(catalog.getCurrentSchemas());

その後でストアの読取りを実行すると、バインディングの作成時には使用されていなかったスキーマを使用するオブジェクトが取得される場合があります。(これは、長時間実行しているクライアント・コードに特にあてはまります)。この問題に対処するには、SchemaNotAllowedExceptionをキャッチします。

/*
 * Key creation and store retrieval skipped.
 */

final GenericRecord animalObject;
if (vv1 != null) {
    try {
        animalObject = binding.toObject(vv1.getValue());
    } catch (SchemaNotAllowedException e) {
        // Take some action here. Potentially you could
        // recreate your binding, and then retry the
        // deserialization process
    }

    /* 
     * Do something with the data. If your client code is 
     * using more than one schema, you can identify which
     * schema the retrieved value is using by testing 
     * the schema name. That is:
     *
     * String sName = animalObject.getSchema().getFullName()
     * if (sName.equals("avro.animalInfo")) { ... }
     */
}