Specific Binding

Generating Specific Avro Classes
Using Avro-specific Bindings
Using Multiple Avro-specific Bindings

The SpecificAvroBinding interface represents values as instances of a generated Avro class which implements SpecificRecord. A single schema binding is created using AvroCatalog.getSpecificBinding(). A multiple schema binding is created using AvroCatalog.getSpecificMultiBinding().

Avro-specific classes provide type safety and ease of use. Properties are accessed through getter/setter methods with parameters and return values of the correct type, as defined in the schema.

Further, when using Avro-specific classes, the client application does not need to be aware of the Avro schema at runtime. The generated class has an internal reference to its associated schema, and this schema is used by the binding, so the application does not need to explicitly supply a schema. Schemas are supplied at build time, when the source code for the specific class is generated.

The disadvantage to using specific bindings is that the set of specific classes is fixed at build time. Applications that wish to treat values generically, or dynamically based on the schema, should use a generic or JSON binding instead.

Generating Specific Avro Classes

When you use a specific binding, you provide your schema to your application using a generated class. You can do this using the org.apache.avro.compiler.specific.SchemaTask tool.

An example Ant file that uses SchemaTask can be found in KVHOME/examples/avro/generate-specific.xml. It automatically downloads all library dependencies required to generate the Avro class, and then creates generated classes for all schema found in the local directory. The example assumes your schema is contained in flat text files that use the avsc suffix.

For example, suppose you had a schema defined in a file named my-schema.avsc. Then to generate an Avro-specific class, place generate-specific.xml and my-schema.avsc in the same directory and run:

ant -f generate-specific.xml

After Ant downloads all the necessary library dependencies, it will create whatever generated classes are required by the contents of my-schema.avsc. The generated classes are named after the name field in the Avro schema. So if you had the (trivial) schema:

{
    "type": "record",
    "name": "MyInfoString",
    "namespace": "avro",
    "fields": [
        {"name": "ID", "type": "int"}
    ]
} 

the generated class name would be MyInfoString.java. It contains getter and setter methods that you can use to access the fields in the schema. For example, a generated class for the above schema would contain the fields MyInfoString.setID() and MyInfoString.getID().

Using Avro-specific Bindings

To use a schema encapsulated in a generated Avro-specific class, you first provide the schema to the store as described in Managing Avro Schema in the Store.

After that, you access the schema using the generated class. To do this, you need handles to the store, an Avro catalog, and a specific binding:

package avro;

import oracle.kv.KVStore;
import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.SpecificAvroBinding;

...


private final KVStore store;
private final AvroCatalog catalog;
private final SpecificAvroBinding<MyInfoString> binding;

...

/* store configuration and open omitted for brevity */

...

catalog = store.getAvroCatalog();
binding = catalog.getSpecificBinding(MyInfoString.class); 

Having done that, you can use the binding to serialize and deserialize value objects that conform to the generated class' schema. For example, suppose you performed a store get() and retrieved a ValueVersion. Then you can access its information in the following way:

final MyInfoString member;
final int ID;

member = binding.toObject(valueVersion.getValue());

System.out.println("ID: " + member.getID()); 

To serialize the object for placement in the store, you use the generated class' setter method, as well as the binding's toValue() method:

member.setID(2045);
/* key creation omitted */
store.put(key, binding.toValue(member)); 

Using Multiple Avro-specific Bindings

If your client code needs to use multiple specific bindings, then you use AvroCatalog.getSpecificMultiBinding() to support the multiple schemas. For example, suppose your client code required the following two schemas:

{
 "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"}
 ]
} 

Then put Avro.PersonInfo in a file (call it PersonSchema.avsc) and Avro.AnimalInfo in a second file (AnimalSchema.avsc). Add these schemas to your store using the command line interface.

Next, generate your specific schema classes using:

ant -f generate-specific.xml

as discussed in Generating Specific Avro Classes. This results in two generated classes: PersonInfo.java and AnimalInfo.java. To use these classes in your client code, you can use a single binding, which you create using AvroCatalog.getSpecificMultiBinding(). Note that to do this, you must use org.apache.avro.specific.SpecificRecord:

package avro;

import java.util.Arrays;

import oracle.kv.Key;
import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.SpecificAvroBinding;

import org.apache.avro.specific.SpecificRecord;

...

private final KVStore store;
private final AvroCatalog catalog;
private final SpecificAvroBinding<SpecificRecord> binding;

/*
 * Store creation is skipped for the sake of brevity.
 */

...

catalog = store.getAvroCatalog();
binding = catalog.getSpecificMultiBinding(); 

You can then create PersonInfo and AnimalInfo objects, serialize them, and write them to the store, like this:

final Key key1 = Key.createKey(Arrays.asList("person", "11"));
final Key key2 = Key.createKey(Arrays.asList("animal", "11"));

final PersonInfo pi = new PersonInfo();
pi.setFirst("Jack");
pi.setLast("Smith");
pi.setAge(38);
store.put(key1, binding.toValue(pi));

final AnimalInfo ai = new AnimalInfo();
ai.setSpecies("Dog");
ai.setName("Bowzer");
ai.setAge(6);
store.put(key2, binding.toValue(ai));

Retrieval of the stored objects is performed with a normal store get(). However, to deserialize the retrieved objects you must identify the object's type. You can do this using either the Java instanceof operator, or by examining the schema's name, as follows:

final ValueVersion vv = store.get(someKey);
if (vv != null) {
    SpecificRecord sr = binding.toObject(vv.getValue());
    if (sr.getSchema().getFullName().equals("avro.PersonInfo")) {
        PersonInfo o = (PersonInfo) sr;
        /*
         * The object is now deserialized. You can access the object's
         * data using the specific class' getXXX() methods. For example,
         * o.getFirst().
         */
    } else {
        AnimalInfo o = (AnimalInfo) sr;
        /*
         * The object is now deserialized. You can access the object's
         * data using the specific class' getXXX() methods. For example,
         * o.getSpecies().
         */
    }