つかびーの技術日記

(情報)工学修士, 元SIer SE, 現Web系 SEの技術blogです。Scala, Java, JS, TS, Python, Ruby, AWS, GCPあたりが好きです。

Avroでデータのserialize, deserializeをする(Java, Scala)

   

こんにちは、@s_tsukaです。

今回はAvroのシリアライザの話です。

こちらの記事ではAvroのsbtプラグインを使って、entityクラスを自動生成する方法を紹介しました。

Avroは単なるスキーマではなく、シリアライズ・デシリアライズのシステムでもあります。むしろシリアライズこそが本命の機能であると言って良いでしょう。そこが非常に強力です。

今回はそのシリアライズまわりについて触れていきます。

Avro公式のJavaライブラリでシリアライズ

AvroをJavaで利用する場合は、公式のライブラリおよびDocumentが利用できます。

http://avro.apache.org/docs/current/gettingstartedjava.html

MavenやSBTで

<dependency>
  <groupId>org.apache.avro</groupId>
  <artifactId>avro</artifactId>
  <version>1.8.1</version>
</dependency>

こちらのjarの依存を追加するだけです。

利用方法は上記のドキュメントGettingStartedを参考にすると良いと思います。もちろんScalaでも使えます。

Codeの自動生成を併用するタイプと、コードの自動生成を併用しない2つのタイプがあります。どちらも基本的にはこのあたりのコードのように

// Serialize user1, user2 and user3 to disk
DatumWriter<User> userDatumWriter = new SpecificDatumWriter<User>(User.class);
DataFileWriter<User> dataFileWriter = new DataFileWriter<User>(userDatumWriter);
dataFileWriter.create(user1.getSchema(), new File("users.avro"));
dataFileWriter.append(user1);
dataFileWriter.append(user2);
dataFileWriter.append(user3);
dataFileWriter.close();
// Serialize user1 and user2 to disk
File file = new File("users.avro");
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<GenericRecord>(schema);
DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<GenericRecord>(datumWriter);
dataFileWriter.create(schema, file);
dataFileWriter.append(user1);
dataFileWriter.append(user2);
dataFileWriter.close();

dataFileWriteを通して書き込み(append)を行うだけです。

ファイルの先頭にスキーマが、その後にデータがバイナリ形式で書き込まれます。

上の方の自動生成を併用するタイプだとuserオブジェクトをそのままappendできて大変簡潔ですが、下の方のだとGenericRecordにする必要があります。外部から来た(予めクラスを作れない・動的な)データをAvro形式で書き込みたいときは後者を採用することになると思います。

デシリアライズもサンプルが載っているのでそれに従えば良いだけです。

avro4sでシリアライズ

https://github.com/sksamuel/avro4s

自分が調べた限りではavro4sが最も開発が盛んで安定していると思います。

他にもGenslerAppsPod/scalavroもありますが、こちらは開発が停滞しており2016/9/15時点でScala 2.11のjarが存在しません。avro4sのほうが良いと思います。

こちらも使い方は単純でjarの依存を追加するだけです。ほぼREADME通りで良いです。循環参照が発生するクラスについてはRecursive Schemasの節の通り、ちょっとしたimplicitとSchemaForを使う必要があるので注意です。

RecordFormatのto, fromメソッドを利用することでGenericRecordオブジェクトを生成することができますが、AvroOutputStreamでGenericRecordを書き込むことはできません。このあたりは先述のAvro公式JavaライブラリのGenericDatumWriterを使うしかなさそうです。

公式とほぼ同じですがサンプルコードです。

import java.io.{ByteArrayInputStream, File}

import com.sksamuel.avro4s.{AvroInputStream, AvroOutputStream}
import org.apache.commons.io.output.ByteArrayOutputStream

object Avro4sMain {
  def main(args: Array[String]): Unit = {

    val pepperoni = Pizza("pepperoni", Seq(Ingredient("pepperoni", 12, 4.4), Ingredient("onions", 1, 0.4)), false, false, 98)
    val hawaiian = Pizza("hawaiian", Seq(Ingredient("ham", 1.5, 5.6), Ingredient("pineapple", 5.2, 0.2)), false, false, 91)
    val pizzaObjects = Seq(pepperoni, hawaiian)

    val os = AvroOutputStream.data[Pizza](new File("pizzas.avro"))
    os.write(pizzaObjects)
    os.flush()
    os.close()

    val is = AvroInputStream.data[Pizza](new File("pizzas.avro"))
    val deserializedPizzas = is.iterator.toSet
    is.close()
    deserializedPizzas.foreach(println)

    val baos = new ByteArrayOutputStream()
    val output = AvroOutputStream.binary[Pizza](baos)
    output.write(pizzaObjects)
    output.close()
    val bytes = baos.toByteArray

    val in = new ByteArrayInputStream(bytes)
    val input = AvroInputStream.binary[Pizza](in)
    val deserializedPizzas2 = input.iterator.toSeq
    deserializedPizzas2.foreach(println)
    in.close()
  }
}

case class Ingredient(name: String, sugar: Double, fat: Double)
case class Pizza(name: String, ingredients: Seq[Ingredient], vegetarian: Boolean, vegan: Boolean, calories: Int)

お手軽ですね。

avro4sを使えば、確かにScalaでもAvroを上手く使うことができますが、case classを作らなかったり、スキーマが動的に変わるシーンなどでは、必ずしもavro4sを使わなくても良いとは思います。

とはいえ、avro4sはJavaのavroライブラリを内包しているので、avro4s使っておけば間違い無いと思います。

Avroに対応しているソフトウェアはまだまだ少ない方ですが、革新的なフォーマットだと思うので、みなさん使っていきましょう。

 - Java, Scala, ライブラリ ,