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);
}
}
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();
Item item = realm.createObject(Item.class);
両方とも型は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) {
}
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);
new Thread() {
@Override
public void run() {
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);
new Thread() {
@Override
public void run() {
RealmResults<Item> result = realmWrapper.where(Item.class).findAll();
List<Item> entities = toImmutableItems(result);
}
}.start();
}
}
しかしこの場合Threadが終了してもThreadLocalにRealmオブジェクトが保持され続けてしまいます。そこで暫定的にRealmWrapper
にstart()
, 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)
を随所に書きたくない、というのが目的なのでRealmWrapper
にRealmWrapper getInstanceForCurrentThread()
とかを生やして使い終わったらすぐclose()
ってのがまだマシな気がします。スレッド周りは制約をちゃんと加味したうえで設計していかないと辛みがあります。今回のケースは完全に失敗だったなと思います。次やるときはいい感じに設計するぞ!
「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
が既に出ていた。差分眺めた感じ今回書いた点で変更はなさそう。