Apache Avro について知っていることを書いていく その2
Apache Avro について書き下していく記事その 2 です。
本記事では Avro で表現されるデータのプログラミング言語上の表現、特に Java を想定して SpecificData
と GenericData
について触れていきます。
Avro のデータ型とコード生成
前回の記事で触れた通り、 Avro では様々なリッチなデータ型を表現できます。
多くの場合一番外側のスコープでは record
type を使って構造体を定義することになるかなと思います。
Avro スキーマは以下のように事前に JSON 文字列を埋め込むか .avsc ファイルで保存しておくのもアリですが、実行時に動的生成することもできます。
{ "name": "Hello", "type": "record", "fields": [ { "name": "id", "type": "long" }, { "name": "name", "type": "string" } ] }
ここで定義したデータ型を実際にどう扱いましょうか? 例えば Protocol Buffers ではちょうど Avro で .avsc ファイルに記述するように、以下のようにおおむね .proto ファイルにメッセージ型を定義していくことになると思います。
syntax = "proto3"; package com.syucream.example; message Hello { uint64 id = 1; string name = 2; }
その後典型的には protoc
と各言語に対応する protoc plugin
を用いることで、定義したメッセージ型に従ったコードを生成することになります。
$ protoc --java_out=./ hello.proto
// Generated by the protocol buffer compiler. DO NOT EDIT! // source: hello.proto package com.syucream.example; public final class HelloOuterClass { private HelloOuterClass() {} public static void registerAllExtensions( com.google.protobuf.ExtensionRegistryLite registry) { } public static void registerAllExtensions( com.google.protobuf.ExtensionRegistry registry) { registerAllExtensions( (com.google.protobuf.ExtensionRegistryLite) registry); } public interface HelloOrBuilder extends // @@protoc_insertion_point(interface_extends:com.syucream.example.Hello) com.google.protobuf.MessageOrBuilder { /** * <code>uint64 id = 1;</code> */ long getId(); /** * <code>string name = 2;</code> */ java.lang.String getName(); ...
つまりアプリケーションに組み込んで用いる際に事前にコード生成を求められます。
Avro ではコード生成はオプションです。 事前に生成したコードを用いることも、そうでない選択肢を取ることもできます。
基本的なデータ型表現セット GenericData
org.apache.avro.generic.GenericData は Avro のデータ型の Java での表現の基礎となるクラスなどが詰まったユーティリティです。
GenericData
以下に具体的な型に対応したクラスが存在します。
例えば record
なら org.apache.avro.generic.GenericData.Record
といった具合です。
以下では特にこの GenericData.Record
を掘り下げましょう。
まずこのクラスは org.apache.avro.generic.GenericRecord interface を実装しているので、まぁ名前の通りなのですがつまり値を get したり put したりできます。
一見あまり情報量が無さそうなこの interface は、結構色々な Java 製ミドルウェアで頻出します。
またこの get()
の戻り値は Object
型であり、実際どんな型なのか意識するのは get()
を呼び出すコードを書く人の責務になります。
GenericData.Record
オブジェクトを作るには、 Schema
オブジェクトを渡して new します。
Schema
オブジェクトは Schema.Parser
を使って事前定義した JSON 文字列をパースして得るのもよし、動的に Schema.createRecord()
などから生成してもよし、です。
この GenericData.Record
ですが、フィールドの具体的な値は Object[]
型のメンバとして持ち、 get/put などをする時は Schema
オブジェクトの情報からフィールドのインデックスを得てアクセスします。
これはちょうど、 Avro の仕様としてシリアライズした後のバイナリのレイアウトがどうなっているかがスキーマによって決定づけられる(他にレイアウト情報を持たずスキーマを信じる)のに対応しそうです。
GenericData を読み書きする GenericDatumReader/GenericDatumWriter
org.apache.avro.generic.GenericDatumReader を用いることでシリアライズされた状態のバイナリから GenericData 以下の Java のオブジェクトを取り出すことができます。
ジェネリクスのパラメータは、例えば record
型の値を参照するだけでいいなら前述の GenericRecord
を指定するので良いでしょう。
逆に Java のオブジェクトをシリアライズしたい際は org.apache.avro.generic.GenericDatumWriter を使うことができます。
より具体的な使い方は公式のテストコードを見ると参考になります。
Avro のシリアライズ方法とバイナリレイアウトを決めているのはスキーマです。
というわけでもちろん、GenericDatumReader
/ GenericDatumWriter
を用いる際にはスキーマ情報が求められます。
さて実は GenericDatumReader
には二種類のスキーマ、引数名でいうと reader
と writer
を与えることができます。
名前の通り writer
は GenericDatumWriter
を使ってシリアライズする側で用いられた側のスキーマ、 reader
は GenericDatumReader
で読み出す側のスキーマです。
具体的で実用しそうなシナリオとしては、 writer
が古いスキーマでシリアライズする可能性のある環境下で reader
としては新しいスキーマ(古いスキーマからの後方互換性がある)に従う型のオブジェクトとして扱いたい際に二種類のスキーマを与える場合がありそうです。
より安全で高速な表現セット SpecificData
さてこの GenericData
関連のツールですが、値の取り回しは Object 型ですることになるので逐一キャストしなければならず、安全な実装をできるかやキャストするコストが発生する点が気になります。
悪い言い方をすれば、シリアライズ・デシリアライズの操作を含めてスキーマに違反していないことをは保証された値でしかなく、 Java のコード上は例えば record
型なら単なる Map<String, Object>
型オブジェクトと言えそうです。
幸いなことに Avro では GenericData
を使うのとは別に SpecificData
サブクラスを使う道も存在します。
org.apache.avro.specific.SpecificData は GenericData
のサブクラスです。
と言ってもこのクラス自体にはさほど機能は追加されていません。
重要なのはこれを継承した、自動生成された schema specific なクラスです。
SpecificData なクラスのコード生成
最初に述べた通り Avro には事前に .java ファイルのコードを生成しておく道もあります。
コード生成は avro-tools
を通して行えます。試しにこの記事の冒頭に出てきた .avsc からコード生成してみます。
$ java -jar ~/tools/avro-tools-1.8.2.jar compile schema hello.avsc ./ Input files to compile: hello.avsc log4j:WARN No appenders could be found for logger (AvroVelocityLogChute). log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
得られる Java のコードは以下のようになります(一部抜粋)
/** * Autogenerated by Avro * * DO NOT EDIT DIRECTLY */ import org.apache.avro.specific.SpecificData; import org.apache.avro.message.BinaryMessageEncoder; import org.apache.avro.message.BinaryMessageDecoder; import org.apache.avro.message.SchemaStore; @SuppressWarnings("all") @org.apache.avro.specific.AvroGenerated public class Hello extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { private static final long serialVersionUID = -8266440640401408575L; public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Hello\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"name\",\"type\":\"string\"}]}"); public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } ... @Deprecated public long id; @Deprecated public java.lang.CharSequence name; /** * Default constructor. Note that this does not initialize fields * to their default values from the schema. If that is desired then * one should use <code>newBuilder()</code>. */ public Hello() {} ... /** * Gets the value of the 'id' field. * @return The value of the 'id' field. */ public java.lang.Long getId() { return id; } /** * Sets the value of the 'id' field. * @param value the value to set. */ public void setId(java.lang.Long value) { this.id = value; } /** * Gets the value of the 'name' field. * @return The value of the 'name' field. */ public java.lang.CharSequence getName() { return name; } /** * Sets the value of the 'name' field. * @param value the value to set. */ public void setName(java.lang.CharSequence value) { this.name = value; } ...
嬉しいことにフィールドの型に対応した getter/setter が手に入ります!
GenericRecord
interface のメソッドを使うよりこちらの方が安全で若干高速になるはずです。
見てきた通り SpecificData
系サブクラスはコード生成する都合コンパイル時にスキーマが決まっている必要があります。
またスキーマ定義が頻繁に変更され、その度にコード生成し直しまくるのもやや煩雑かと思われます。
あまり変わらない・変えたくないデータ型を定義する時は SpecificData
からなるクラスを生成、動的に・頻繁に変わる可能性があるデータ型は GenericData
として扱うのが良いかもしれません。
Java 意外の言語ではどうなる?
Java の例を見てきた通り Specific なテータ表現は Generic なもののサブセットとなっています。 そんな訳で多くのプログラミング言語では Generic なデータ表現はサポートされています。
が、 Specific によってはサポート状況がまちまちなようです。 筆者が認識した限りでは、 C++ で experimental でサポート? されていたり、 Go では gogen-avro を作っている人がいます。
飽きてきたので
本記事はこのあたりで終わります。 あとは筆者がよくハマる、複雑なスキーマを定義してみる話やシリアライズされた後のバイナリのレイアウトの話とか別記事で書くかもしれないです。