Android用ORMライブラリを書き始めました。
開発の動機
AndroidのORM事情は2014年の天下一「AndroidのORM」武道会 - Qiita あたりをどうぞ。ただ2015年11月現在だとDBFlow 2.xが爆速になっており、GreenDAOに匹敵するレベルになっていそうです。ほかのライブラリもいろいろアップデートしているので、天下一Android ORM武闘会の2015年版が望まれます。
さて本題ですが、私がAndroidのORMに求めるものは下記のようなものです。
- 高速
- Realm並は無理でも、爆速ORMとして知られるGreenDAO程度の速度はほしい
- upgrade / downgradeできるmigration機構
- データベースハンドルがオブジェクトであってほしい
- static methodsベースだとmockできないしライブラリからも使えない
- POJO modelもサポートしてほしい
- modelの実装になるべく制限を加えたくない
- modelをParcelableにしたい
- 基本的には都度selectしてくるのが正しいものの、fragmentの引数としてカジュアルに使うくらいはしたい
- has-one / has-many relationshipを簡単に実装したい
- スキーマをソースコードに書きたい
この条件を満たす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.
// 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はほぼおなじ速度でした。
Ormaのベンチマーク、Realmのコードでクエリ発行してなかった。直したらOrmaとRealmはほぼ同じ速度になった。 / “fix benchmark; Realm executed no query! by gfx · P…” https://t.co/FXuxMBPBCQ
— Fuji, Goro (@__gfx__) November 22, 2015
よって以下は古い情報です。
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でているので三者とも十分な速度はでていると思います。