Islands in the byte stream

Technical notes by a software engineer

AndroidのORMに求めること、あるいはOrmaを開発した話

Android用ORMライブラリを書き始めました。

github.com

開発の動機

AndroidのORM事情は2014年の天下一「AndroidのORM」武道会 - Qiita あたりをどうぞ。ただ2015年11月現在だとDBFlow 2.xが爆速になっており、GreenDAOに匹敵するレベルになっていそうです。ほかのライブラリもいろいろアップデートしているので、天下一Android ORM武闘会の2015年版が望まれます。

さて本題ですが、私がAndroidのORMに求めるものは下記のようなものです。

  • 高速
    • Realm並は無理でも、爆速ORMとして知られるGreenDAO程度の速度はほしい
  • upgrade / downgradeできるmigration機構
    • なるべく自動的によしなにやってくれるのがよい
    • たとえば開発中にカラムを追加したときは自動的にmigrationしてほしい
    • マイグレーションコードを書かなければいけないとしたらJavaで書きたい
  • データベースハンドルがオブジェクトであってほしい
    • static methodsベースだとmockできないしライブラリからも使えない
  • POJO modelもサポートしてほしい
    • modelの実装になるべく制限を加えたくない
  • modelをParcelableにしたい
    • 基本的には都度selectしてくるのが正しいものの、fragmentの引数としてカジュアルに使うくらいはしたい
  • has-one / has-many relationshipを簡単に実装したい
  • スキーマソースコードに書きたい
    • 独自DSLをおぼえるのはコストが高い
    • JSON / YAMLはありかもしれないが補完が効かないのはつらい

この条件を満たすORMがありません。Realmはmodel objectの制約がつらいし、GreenDAOは導入が大変すぎるし、DBFlowのインターフェイスははstatic methodだし、ActiveAndroidは遅すぎます。

そこで、これらをすべて満たしたORMとしてOrmaを開発することにしたのでした。現状、migration以外はだいたい実装できてます*1

使い方は簡単で、以下のようなモデルをつくって一度ビルドすると OrmaDatabase が生成されるので…

package com.github.gfx.android.orma.example;

import com.github.gfx.android.orma.annotation.Column;
import com.github.gfx.android.orma.annotation.PrimaryKey;
import com.github.gfx.android.orma.annotation.Table;

import android.support.annotation.Nullable;

@Table
public class Todo {

    @PrimaryKey
    public long id;

    @Column(indexed = true)
    public String title;

    @Column
    @Nullable
    public String content;
}

あとはOrmaDatabaseを通じて操作します。

e.g.

https://github.com/gfx/Android-Orma/blob/master/example/src/main/java/com/github/gfx/android/orma/example/BenchmarkActivity.java

// DB handleの生成
OrmaDatabase orma = new OrmaDatabase(context, "orma.db");

操作はすべてこのormaインスタンスを通じて行います。つまりOrmaはActive Record patternを実装していません。

insertは単発でもできますし、prepared statement + transactionで高速にbulk insertもできます。

// bulk insert
orma.transaction(new TransactionTask() {
    @Override
    public void execute() throws Exception {
        Inserter<Todo> sth = orma.prepareInsertIntoTodo();

        for (int i = 0; i < N; i++) {
            Todo todo = new Todo();

            todo.title = "title " + i;
            todo.content = "content content content content" + i;

            sth.execute(todo);
        }
    }
});

selectもこんな感じで selectFromTodo() みたいなメソッドがモデルごとに生成されます。本当は where(todo -> todo.title.equals("title")) とかできると最高なんですが、それはかなり難しい*2のでおそらくやりません。

// select
List<Todo> = orma.selectFromTodo()
  .where("title = ?", title)
  .order("id DESC")
  .toList();

updateとdeleteも自動生成されたクラス経由で行います。

orma.updateTodo()
  .where("title = ?", title)
  .content("modified") // set content = "modified"
  .execute()

orma.deleteFromTodo()
  .where("title = ?", title)
  .execute()

migrationができるようになるまでは実用的ではありませんが、現状そこそこ使えます。

Benchmark

追記: Realmではselectクエリを発行してませんでした。正しく測りなおしたらRealmとOrmaはほぼおなじ速度でした。

よって以下は古い情報です。


DBFlowとRealmを相手にベンチマークもとってみました。10,000件のinsertと、10,000件のselectです*3

insertは三つとも横並びで、実行するたびに順位が入れ替わる程度です。SQLiteベースでもRealmに匹敵する速度は出せるということですね。

selectAllは Realm >> Orma > DBFlow で、これは何度か実行してもこの順番です。これはさすがにRealmの面目躍如といったところですし、SQLiteベースでこれ以上の速度は難しいでしょう。

とはいえ、insertで20,000qps*4、selectAllで70,000qpsでているので三者とも十分な速度はでていると思います。

f:id:gfx:20151114201742p:plain

*1:まあmigrationが一番難しいんですがね!

*2:Javaの式をパースしてSQLに変換しないといけない

*3:example appのdrawer menuにある "benchmark" で実行できます

*4:query per second