Subscribed unsubscribe Subscribe Subscribe

Islands in the byte stream

Technical notes by a software engineer

ActiveAndroidからOrmaに移行するための4つのステップ

だいたいOrmaでAcitiveAndroid (AA) を置き換える準備が整ったので、手順を書いておきます。

Table of Contents

Step 1 - インストール

build.gradleのdependenciesを書き換えます。最新版はOrmaの公式サイトで確認してください。

-    compile 'com.michaelpardo:activeandroid:+'
+    apt 'com.github.gfx.android.orma:orma-processor:2.+'
+    compile 'com.github.gfx.android.orma:orma:2.+'

Step 2 - スキーマ定義

以下のことを念頭に置いてModelの @Table@Column を書き換えます。

  • AAの @Table(name = "...") は Ormaでは @Table("...") または @Table(value = "...") と書く。ただしテーブル名がクラス名と一致するなら名前は不要。なおOrmaはリフレクションを使わないので、ProGuardの影響をうけません
  • AAの @Column(name = "...") は Ormaでは @Column("...") または @Column(value = "...") と書く。ただしカラム名がフィールド名と一致するなら名前は不要
  • AAの @Column(... unique = true, onUniqueConflict = Column.ConflictAction.REPLACE) は Ormaでは @Column(... uniqueOnConflict = OnConflict.REPLACE) と書く
  • AAは Id INTEGER PRIMARY KEY カラムを暗黙のうちに定義するが、Ormaはすべて明示しなければならない。つまり @PrimaryKey long id; が必要。なおSQLiteでは名前は大文字・小文字を区別しないので idId は同じ意味
  • Ormaでprimary keyでソートしたい場合は @PrimaryKey(autoincrement = true) が必須。これは、そもそもautoincrementでないprimary keyは順番に意味を持たないため*1
  • Ormaでカラムを検索したりソートしたり場合は、 @Column(... indexed = true) が必須。これは、INDEXのないカラムで検索やソートをすべきでないため
  • AAだとあらゆるカラムがNULL制約なし(つまりnullable)で定義されるが、OrmaではデフォルトでNOT NULL制約がつく。DBのカラムをnullableにしたいときはスキーマクラスのフィールドに @Nullable 注釈をつけること

ここまで作業したら一旦ビルドを走らせてOrmaDatabaseやヘルパークラスを生成します。コンパイルエラーが残っていても、スキーマ定義があればヘルパークラスは生成されると思います。

Step 3 - 初期化

OrmaDatabaseインスタンスはアプリケーション単位でシングルトンにするといいでしょう。以下は初期化のためのテンプレです。なおデバッグビルドだとかなり詳細にログがでますが、リリースビルドでは自動的にログは抑制されます。

また、Ormaにはメインスレッドでの読み込みで警告を、書き込みで例外を発生させる機能があります。しかしAAを既に使っているアプリだとその制限が強すぎるので、弱めるほうがいいでしょう。

public static OrmaDatabase createOrmaInstance(@NonNull Context context, @Nullable String name) {
    return OrmaDatabase.builder(context)
            .name(name)
            .migrationEngineStep(3, new ManualStepMigration.ChangeStep() {
                @Override
                public void change(@NonNull ManualStepMigration.Helper helper) {
                   // AAのmigration file (e.g. 3.sql) の中身のうち、必要なものだけを書く
                   // CREATE TABLEは自動で行われるので、「データが消えてもいいので単にテーブルを作り直したい」というテーブルは消すだけでよい
                    helper.execSQL("DROP TABLE IF EXISTS FooBarLog");
                }
            });
            .typeAdapters(typeAdapters)
            .readOnMainThread(AccessThreadConstraint.NONE)
            .writeOnMainThread(BuildConfig.DEBUG ? AccessThreadConstraint.WARNING : AccessThreadConstraint.NONE)
            .build();
}

AAのserializerを使っている場合、Ormaではtype adapterが必要です。type adapterについてはREADMEをご覧ください。

https://github.com/gfx/Android-Orma#static-type-adapters

Step 4 - CRUD操作

アノテーションプロセッシングで生成される ${モデル名}_Relation というヘルパークラスを、モデルごとのCRUD操作のエントリポイントにできます。Relationクラス自体は、あるテーブルに対してフィルタ条件とソート条件を設定可能なオブジェクトです。

たとえば、以下のようなTodoクラスがあるとき:

@Table
public class Todo {
    @PrimaryKey(autoincrement = true)
    public long id;

    @Column(indexed = true)
    public String title;

    @Column
    @Nullable
    public String content;

    @Column(indexed = true)
    public boolean done;

    @Column
    public Timestamp createdTime;
}

レコードの生成順と逆順でRecyclerViewに表示するのであれば、次のようにRelationインスタンスを生成し、それを通じてモデルを操作することになるでしょう。

@Table
public class Todo {

  // ...

  public static Todo_Relation relation() {
    OrmaDatabase orma = OrmaHolder.getOrmaI(); // どこかにOrmaDatabaseのインスタンスがあるとする
    return orma.relationOfTodo().orderByIdDesc();
  }
}

あるモデルに関するすべての操作はRelationから行えます。詳細は OrmaのREAMDEより: GitHub - gfx/Android-Orma: A lightning-fast ORM for Android as a wrapper of SQLiteDatabase

AAと比較した注意点は以下のとおりです。

  • AAと異なり、「INSERTまたはUPDATEを行う」という意味での save() はOrmaにはない。かわりに primary keyやuniqueに対する OnConflict.REPLACE でほとんどの場合代替できる
  • @PrimaryKey(auto = true) となっていると(これがデフォルトの挙動)、INSERTでそのカラムは無視される。これを無視しないようにするには(つまりPKで INSERT OR UPDATE するには)、 Relation#upserter() を使う

以上を踏まえると AAの save()delete() はほとんどの場合、以下のようになります。いずれもモデル固有のヘルパークラスを経由するので、モデルの基底クラスに寄せることはできません。

  public void save() {
    if (id == 0) {
      relation().inserter().execute(this);
    } else {
       relation().upserter().execute(this);
    }
  }

  public void delete() {
    relation().deleter().idEq(id).execute();
  }

また、Relationインスタンスをうけとる RecyclerView.Adapter のサブクラスである OrmaRecyclerViewAdapter と、 BaseAdapter のサブクラスである OrmaListAdapter が提供されているので、RecyclerViewやListViewなどでRelationを直接表示できます。

使い方はexampleを参照のこと。

その他注意点

  • Ormaのmigrationは、カラムの追加・削除・型や制約の変更などは自動的に検知されてalter tableやテーブルの再生成が行われる。しかしmigrationはデータ量に応じた時間がかかるためテーブルによっては注意深く行うこと。migrationの発動を最小限に抑えるため、 @Column(storageType = "INTEGER") などの設定を行える
    • 置きかえる段階ではリスクを最小限に抑えるため、テーブルを再構築するmigrationは発動しないようにするほうがいい
  • 自動マイグレーションだけでなく、手動でステップを定義するマイグレーションも用意されている(OrmaDatabase.Builder#migrationStep())。
  • マイグレーションのログはデバッグビルドでかなり詳細に出るのでデバッグは比較的容易なはず
  • 後々のカラムの追加では、必ず Column#defaultExpr を設定する必要がある。さもないと自動マイグレーション時にクラッシュする
  • OrmaはRobolectric・Android両方で動作する。テストのときはdatabase nameをnullにするとオンメモリDBになるので、テストケースごとのcleanupの手間を省ける

*1:autoincrementを指定するとINSERTが少し遅くなりますが、気にするほどではありません。