ことのはじまり
現在Realmを使ってAndroidアプリを作っているのだが、DB構成をいじりたいことが多々ある
↓
しかし、デバッグ段階のアプリとは言え既に実用的にアプリを使い始めているのでデータが消えるのは困る
↓
やることと言えば、大体カラムを増やすことぐらいなので、いちいちRealmMigrationを調べて書くのは面倒
↓
RealmObjectのリストをjson化してPreferencesに保存し、復元してRealmObjectにしてupsertすればいいんじゃないと思う
↓
やってみよう
道のり
ひとまずの構成
ItemRealmObject
これは普通のオブジェクトクラス
アクセスクラス
RealmHelperというクラスを作成してアクセスを行う
public class ItemRealmHelper extends AbstractRealmHelper {
public static void insertOneShot(ItemRealmObject itemRealmObject) {
executeTransactionOneShot(insertTransaction(itemRealmObject));
}
private static Realm.Transaction insertTransaction(final ItemRealmObject itemRealmObject) {
return new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.insertOrUpdate(itemRealmObject);
}
};
}
@Override
public void upsert(final ItemRealmObject item) {
item.updateDate = new Date();
executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.copyToRealmOrUpdate(item);
}
});
}
@Override
public RealmResults<ItemRealmObject> findAll() {
return mRealm.where(ItemRealmObject.class).findAll();
}
}
public abstract class AbstractRealmHelper<T extends RealmObject> {
protected final Realm mRealm;
public AbstractRealmHelper() {
mRealm = getRealm();
}
static Realm getRealm() {
return Realm.getInstance(new RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build());
}
protected static void executeTransactionOneShot(Realm.Transaction transaction) {
Realm realm = getRealm();
realm.executeTransaction(transaction);
realm.close();
}
public abstract void upsert(T t);
public abstract RealmResults<T> findAll();
protected void executeTransaction(Realm.Transaction transaction) {
mRealm.executeTransaction(transaction);
}
public void destroy() {
mRealm.close();
}
}
『最初は、jsonにして戻すだけでしょ~』と、軽い気持ちだった
Realmのドキュメントを読んでみる
https://realm.io/jp/docs/java/latest/#gson
GSONは、JSONをシリアライズ/デシリアライズするGoogle製のライブラリです。GSONは特別な設定等は不要でそのままRealmとともに使用可能です。
なるほど。行けるじゃん。
今回はまず、Preferencesに保存したいので、さっそくシリアライズしてみる。
new Gson().toJson(RealmObject);
すると、
StackOverFlowError
だめじゃん・・・。
再びドキュメントを見てみる。
シリアライズについては、GSONのデフォルトのままではRealmオブジェクトをシリアライズすることはできません。 GSONはgetter/setterを使わず、直接インスタンス変数を参照するためです。
GSONを使ったRealmオブジェクトのJSONシリアライズを正しく動作させるためには、それぞれのモデルに対してカスタムJsonSerializerとTypeAdapterを書く必要があります。
このGistにあるコードは、それらをどのように書けばよいかを示しています。
なんか書かなくちゃいけないのか・・・。やだな。
デシリアライズはRealmObjectに対してできるのか。
であれば・・・。
ということで、同じRealmObjectじゃないモデルクラスを作って、それをJson化することに。
同じフィールド名を持つモデルクラスを作り、コンストラクタにRealmObjectを渡したら各フィールドに値を入れられるように設定。
RealmHelperのfindAllで取ってきたRealmResultsをfor文で回してモデルクラスのリストを作成し、それをJson化してPreferencesに保存。
void save() {
writeData(PrefKey.itemRealm, new ItemRealmHelper());
writeData(PrefKey.priceRealm, new PriceRealmHelper());
}
private <T extends RealmObject> void writeData(Enum key, AbstractRealmHelper<T> helper) {
RealmResults<T> results = helper.findAll();
List<Object> list = new ArrayList<>();
for (T t : results) {
list.add(getModel(t));
}
PreferenceUtils.writeValue(getContext(), key, JsonUtils.toJson(list));
helper.destroy();
}
// 増えたらここに記述を追加
private Object getModel(Object o) {
if (o instanceof ItemRealmObject) {
return new ItemModel((ItemRealmObject) o);
} else if (o instanceof PriceRealmObject){
return new PriceModel((PriceRealmObject) o);
}
throw new InternalError();
}
(このコードは一度下のloadメソッドを作ってからリファクタリングしたので、元はもう少し煩雑だった)
・・・さて、保存はうまく行った!
さーて、後は戻すだけ
戻すためのコードを書いてみる
ItemRealmHelper itemHelper = new ItemRealmHelper();
String json = PreferenceUtils.readValue(getContext(), PrefKey.itemRealm, "");
List<ItemRealmObject> itemModelList = new Gson().fromJson(json, new TypeToken<List<ItemRealmObject>>() {}.getType());
for (ItemRealmObject itemModel : itemModelList) {
itemHelper.upsert(itemModel);
}
お、行けた。
しかしここでふと考える。
『RealmObjectはいくつか作ってるから、愚直に書いた場合、このままコードが増えていくのはやだし、毎回コピーして型変えていくのも面倒だな・・・』
// 愚直に書いた場合増えていく・・・
void load() {
ItemRealmHelper itemHelper = new ItemRealmHelper();
String json = PreferenceUtils.readValue(getContext(), PrefKey.itemRealm, "");
List<ItemRealmObject> itemModelList = new Gson().fromJson(json, new TypeToken<List<ItemRealmObject>>() {}.getType());
for (ItemRealmObject itemModel : itemModelList) {
itemHelper.upsert(itemModel);
}
itemHelper.destroy();
Item2RealmHelper item2Helper = new Item2RealmHelper();
String json2 = PreferenceUtils.readValue(getContext(), PrefKey.item2Realm, "");
List<Item2RealmObject> item2ModelList = new Gson().fromJson(json, new TypeToken<List<Item2RealmObject>>() {}.getType());
for (Item2RealmObject item2Model : item2ModelList) {
item2Helper.upsert(item2Model);
}
item2Helper.destroy();
}
// ...以下増える度に増える
・・・と、言うことで。
ジェネリクスの出番
upsertでメソッド化しようということで作ってみた。
private <T extends RealmObject> void upsert(Enum key, TypeToken<T> typeToken, final AbstractRealmHelper<T> helper) {
List<T> list = new Gson().fromJson(PreferenceUtils.readValue(getContext(), key, ""), new TypeToken<List<T>>() {}.getType());
for (T t : list) {
helper.upsert(t);
}
helper.destory();
}
よし、うまく書けたぞ。ということで実行。
ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast
なんだこれは・・・。
調べてみると記事が出てきた。
GsonでJSON文字列をGenericsなオブジェクトに変換するときにハマる
http://osa030.hatenablog.com/entry/2015/10/20/182439
読み進めると、
GsonのfromJsonの結果はHogeクラスまではデコードできているんだけど、valueがFugaクラスではなくて「com.google.gson.internal.LinkedTreeMap」になっている。
つまり、List型にはなっているけど、その先はLinkedTreeMapという状態らしい。
Once your .java source file is compiled, the type parameter is obviously thrown away. Since it is not known at compile time, it cannot be stored in bytecode, so it's erased and Gson can't read it.
要するに
"<T>"はビルド時に消えるので動的には見えねよバーカバーカ。だから親切心でLinkedTreeMapにしといてやったわー。
StackOverFlowのリンクが貼られていたので見てみる。
Java Type Generic as Argument for GSON
https://stackoverflow.com/questions/14139437/java-type-generic-as-argument-for-gson/14139700#14139700
これはListに変換したい例なので自分と合致する。
StackOverFlowにあった、ListOfSomethingをコピってきて使ってみる。
private <T extends RealmObject> void upsert(Enum key, Class<T> clazz, final AbstractRealmHelper<T> helper) {
List<T> list = new Gson().fromJson(PreferenceUtils.readValue(getContext(), key, ""), new ListOfSomething<>(clazz));
for (T t : list) {
helper.upsert(t);
}
helper.destory();
}
お。できた。すごい。
中身を見てみると、getRawTypeでList.classを固定で返している。
だから一つ目の記事の人はGenericOfクラスを作ったのか、と理解。
せっかくなので汎用性を考えて(?)変えてみる。
private <T extends RealmObject> void upsert(Enum key, Class<T> clazz, final AbstractRealmHelper<T> helper) {
List<T> list = new Gson().fromJson(PreferenceUtils.readValue(getContext(), key, ""), new GenericOf<>(List.class, clazz));
for (T t : list) {
Footprint.fields(t);
helper.upsert(t);
}
helper.destory();
}
最高にスッキリした。
これで、loadメソッドではupsertだけ呼べばいいので、7行→1行になり、例えば2つのRealmObjectがあっても1行ずつ追加するだけ!
void load() {
upsert(PrefKey.itemRealm, ItemRealmObject.class, new ItemRealmHelper());
upsert(PrefKey.item2Realm, Item2RealmObject.class, new Item2RealmHelper());
}
スッキリ簡単になってめでたしめでたし。
まとめ
void save() {
writeData(PrefKey.itemRealm, new ItemRealmHelper());
writeData(PrefKey.priceRealm, new PriceRealmHelper());
}
private <T extends RealmObject> void writeData(Enum key, AbstractRealmHelper<T> helper) {
RealmResults<T> results = helper.findAll();
List<Object> list = new ArrayList<>();
for (T t : results) {
list.add(getModel(t));
}
PreferenceUtils.writeValue(getContext(), key, JsonUtils.toJson(list));
helper.destroy();
}
// 増えたらここに記述を追加
private Object getModel(Object o) {
if (o instanceof ItemRealmObject) {
return new ItemModel((ItemRealmObject) o);
} else if (o instanceof PriceRealmObject){
return new PriceModel((PriceRealmObject) o);
}
throw new InternalError();
}
void load() {
upsert(PrefKey.itemRealm, ItemRealmObject.class, new ItemRealmHelper());
upsert(PrefKey.item2Realm, Item2RealmObject.class, new Item2RealmHelper());
}
private <T extends RealmObject> void upsert(Enum key, Class<T> clazz, final AbstractRealmHelper<T> helper) {
List<T> list = new Gson().fromJson(PreferenceUtils.readValue(getContext(), key, ""), new GenericOf<>(List.class, clazz));
for (T t : list) {
helper.upsert(t);
}
helper.destory();
}
最後に
Gsonでリストを含むクラス指定をするためにTypeTokenを使えるようになって満足していたけど、そこにジェネリクスを絡ませるとParameterizedTypeというものが必要なことが分かった。
まだまだGsonもジェネリクスも奥が深い。
これ、ListのList型をジェネリクスで取りたい場合はどうするんだろう?と考えたけどヤバそうなので考えるのはやめた。
(GenericsOfの引数を2つともTypeTokenにすればいいのかな・・・?)
こんなにかかるんだったらJsonSerializerとTypeAdapterを書いてRealmObjectをシリアライズできるようにした方がいいのでは?と思ったのは内緒。
さらに、そもそもMigration書いたほうが早かったのでは?というのはもっと内緒。
勉強になったから良しということで・・・。