読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

ここがつらいよ realm-android 0.81.1

Android Realm

Realmわりといいんですが「簡単!便利!スナック!」というイメージだったのでスナックボリボリする感じで適当に書いてたらガンガンクラッシュ*1して「スナックじゃないな?ぬか漬けかな?」みたいな気持ちになりました。とりあえず一旦ぬか漬け*2部分をまとめます。将来改善予定のものもあれば、自分の設計が間違ってるんだろうなぁーというものもあります。io.realm:realm-android:0.81.1を使っての感想です。iOS版の使い心地についてはわかりません。

autoincrementがない

今のところautoincrementがないです。how to set an auto increment id? · Issue #469 · realm/realm-java · GitHubとか眺めると「まだサポートしてないぜ!」との事。将来サポートされるでしょう。とりあえず現状は以下のような仕組みをつくってしのいでます。PrimaryKeyがstringの場合は使えないです。

public class AutoIncrement {
  public static long newId(Realm realm, Class<? extends RealmObject> clazz) {
    return newIdWithIdName(realm, clazz, "id");
  }

  public static long newIdWithIdName(Realm realm, Class<? extends RealmObject> clazz, String idName) {
    return realm.where(clazz).maximumInt(idName) + 1;
  }
}

こんな感じで使ってます。

realm.beginTransaction();
Item item = realm.createObject(Item.class);
item.setId(AutoIncrement.newId(Item.class));
// 略
realm.commitTransaction();

PrimaryKeyを0で保存すると次のRealm.createObjectの時点で衝突して死ぬ

Realm.createObject(Class<E>)は指定したE型の空レコードを生成して返してくれます。この時short, int, longのフィールドは0で初期化します。すでにPrimaryKey=0のレコードが存在する場合以下のエラーを吐いて死にます。

io.realm.exceptions.RealmException: Primary key constraint broken. Value already exists: 0

まぁかならずPrimaryKeyはセットしましょうって事なんですがRealm.createObject()で死ぬので最初意味がわかりませんでした。前項のautoincrementの仕組みとか使えば問題にはならないです。

mockできない

Realmクラスはmockitoでmockできません。

Realm realm = mock(Realm.class);

Realmクラスがfinalだからです。いやーmockしたい。仕方ないのでwrapperを作ってcompositionにしました。

public class RealmWrapper {

  Realm realm;

  public RealmWrapper(Context context) {
    realm = Realm.getInstance(context);
  }

  public <E extends RealmObject> List<E> copyToRealm(Iterable<E> objects) {
    return realm.copyToRealm(objects);
  }
  //以下Realmクラスが持つpublicメソッド全てをDelegateする
}

Android StudioのGenerate-Delegate Methods...で一発でいけます。

RealmWrapper realm = mock(RealmWrapper.class);
when(realm.createObject(ItemIndex.class)).thenReturn(new ItemIndex());

やったね。まぁよく考えたらRealmに直接さわるクラスを限定してそいつをmockableにしたらいいんじゃないかな?という気もしますが。そういう意味ではActivityやFragmentやThreadで直接Realmオブジェクト触るのはあんまり良くないんじゃないかなぁという印象です。

スレッド制約

Realmはスレッドの制約がつらいです。近々マルチスレッドをサポートするそうなので期待ですね。現状なにが辛いかというと以下の点です。

  • RealmオブジェクトはgetInstance()したスレッドでしか使えない。別スレの場合そのスレッドでgetInstance()しないといけない
  • where等のRealmQueryで得たRealmResultを別のスレッドに渡せない。別スレで触ると死ぬ
  • RealmResult等から取り出したオブジェクト(いわゆるmanaged object)を別スレで触ると死ぬ

例えばItemというモデルを定義した場合以下の様なクラスが生成されます。これをmanaged objectと呼ぶらしいです。

public class ItemRealmProxy extends Item
    implements RealmObjectProxy {

Realm.createObject(Classs<E>)やクエリなどで引っ張ってきたオブジェクトの実体はこのmanaged objectになります。でこのオブジェクトは内部にRealmオブジェクトを持っていてgetter、setterを実行する際にcheckIfValid()でスレッドの検査をしている為別スレッドで触れないです。あんまり深く追っかけてないですが、スレッドの検査を通過したらnative methodを呼んでいるのでgetter、setterが呼ばれた時にはじめてDBからデータの読み書きしてるのかな?という感じです。

この制約によって以下の2つの問題と戦う事になりました。

  • 別スレで取り出したmanaged objectをUIスレッドにどうやって持ってくるか?
  • Realmオブジェクトの管理をどうするか。Dagger等でinjectionする場合スコープをどうするべきなのか?
    • ActivityにRealmをInjectすると、別スレで処理する時に死ぬ。

managed objectをUIスレッドにどうやって持ってくるか

まず問題と思ったのはmanaged objectとstandalone objectの区別が付かない点でした。

Item item = new Item(); // standalone object
Item item = realm.createObject(Item.class); //managed object

両方とも型はItemなので混乱するなぁという事でstandalone objectを持つクラスを別途用意しました。別の型である方がよいのでcompositionで作りました。最初はItemを継承したんですがこれだとコンパイルが通りませんでした*3

public class ImmutableItem {
    Item item;
    //...
}

値の初期化については最初gsonを使って以下のように書いていたんですが、

public ImmutableItem(Item item) {
  String json = gson.toJson(item);
  this.item = gson.fromJson(json, Item.class);
}

managed objectのデータアクセスはgetter、setterを通さないとダメなのでスカスカのデータにしかなりませんでした。RealmとJSONライブラリ // Speaker Deckにある通りTypeAdapterを使えばgsonでもやれるんですが意味がないので愚直に書くことにしました。

public ImmutableItem(Item item) {
  this.item = new Item();
  this.item.setId(item.getId());
  // 略
}

standalone objectである事を保証する型を作ることでAPIの返却値やObservableとかで扱いやすくなりました。

public Observable<List<ImmutableItem>> items(int page, int perPage) {
  //...
  //subscribeOn(Scheduler.io())なんかもどんとこい
}

Realmオブジェクトの管理をどうするか

RealmオブジェクトをDaggerで注入とかやってたので辛かったです。幸いRealmWrapperを作っていたのでThreadLocalを使ってゴニョゴニョする事にしました。具体的にはこんな感じ。

public class RealmWrapper {

  ThreadLocal<Realm> realms = new ThreadLocal<Realm>();

  public Realm getRealm() {
    Realm realm = realms.get();
    if (realm == null) {
      realm = Realm.getInstance(context);
      realms.set(realm);
    }
    return realm;
  }

これで触るスレッドを意識しなくてよくなるんですが思いっきりメモリリークします。この辺は後述するライフサイクル周りの話でなんとかしました。が良い方針ではないのでマネしないほうがいいと思います。

ライフサイクルがよくわからない

Realmオブジェクトってどのくらいの頻度でgetInstance()すべきなのか?ドキュメントにはonDestroy()close()しろとあるのでActivityくらいのスコープでやってたらいいのかなと思ったら当然以下のコードはダメですよね。

class AwesomeActivity exetends Activity {
  Realm realm;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    realm = Realm.getInstance(this); // UI Thread
    new Thread() {
        @Override
        public void run() {
          // 異なるスレッドでrealmに触れているのでクラッシュする
          RealmResults<Item> result = realm.where(Item.class).findAll();
          List<Item> entities = toImmutableItems(result);
          //...
        }
    }.start();
  }
}

前項のThreadLocalを使ったRealmWrapperなら問題にはなりません。

class AwesomeActivity exetends Activity {
  RealmWrapper realmWrapper;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    realmWrapper = RealmWrapper.getInstance(this); // UI Thread
    new Thread() {
        @Override
        public void run() {
          // 内部でThreadLocalでRealmを触るのでクラッシュしない
          RealmResults<Item> result = realmWrapper.where(Item.class).findAll();
          List<Item> entities = toImmutableItems(result);
          //...
        }
    }.start();
  }
}

しかしこの場合Threadが終了してもThreadLocalにRealmオブジェクトが保持され続けてしまいます。そこで暫定的にRealmWrapperstart(), end()ってのを生やしました。

public void start() {
  realms.set(null);
}

public void end() {
  Realm realm = getRealm();
  realm.close();
  realms.set(null);
}

とりあえずこれで動いたんですが、これは良くない設計です。確実にstart()end()を忘れるケースが出るでしょう。

//こんな感じで使う
realmWrapper.start();
RealmResults<Item> result = realmWrapper.where(Item.class).findAll();
List<Item> entities = toImmutableItems(result);
realmWrapper.end();

直接Realm.getIntance(Context)を随所に書きたくない、というのが目的なのでRealmWrapperRealmWrapper getInstanceForCurrentThread()とかを生やして使い終わったらすぐclose()ってのがまだマシな気がします。スレッド周りは制約をちゃんと加味したうえで設計していかないと辛みがあります。今回のケースは完全に失敗だったなと思います。次やるときはいい感じに設計するぞ!

json制約

「managed objectをUIスレッドにどうやって持ってくるか」の所でも書きましたがjson周りに辛みがあります。RealmとJSONライブラリ // Speaker Deckに全部書いてあります。当面はtoJson()はしない方向で頑張る方針にしました。

おわりに

いろいろ触る前にJava Docs - Realm is a mobile database: a replacement for SQLite & Core Dataを熟読するべきだったなという感じです。ゲッター/セッターメソッドは RealmObject が作るプロキシクラスによって上書きされることです。 ゲッター/セッターに書かれたロジックは実行されません。とか割と重要な事サラッと書いてある。学習した今だと色々もっと設計でカバーできそうだなぁという感じです。色々つらい目にあったけどRealm良いと思うので「私はRealmを続けるよキャンペーン」です。version見たら0.82.1が既に出ていた。差分眺めた感じ今回書いた点で変更はなさそう。

*1:使い方が間違っているとクラッシュします。良いことだと思います

*2:ぬか床の手入れって大変だそうですね

*3:A RealmClass annotated object must be derived from RealmObjectと言われます