通信キャッシュのDB処理設計の考察
はじめに
今更遅れてDataBinding事始め(5.5)[コピペ用] - exception think
で書いた状態だと、アプリ終了時に保持していたデータがやはり消えるわけで、DB保存はしたいわけです
でそこら辺に関する考察をしてみます
Realm共通処理
DB接続処理等の定形記述を集めたクラス
まあここらへんはいつもの定形処理なので普通に説明は割愛
- RealmCommon
public class RealmCommon { //RealmDB インスタンスを取得 public static synchronized Realm getInstance() { Realm realm = Realm.getDefaultInstance(); return realm; } //Applicationの全体共通設定 public static void setDefaultRealmConfiguration(Context context, @NonNull String filename, int schemaVersion) { setDefaultRealmConfiguration(context,filename,schemaVersion,null); } public static void setDefaultRealmConfiguration(Context context, @NonNull String filename, int schemaVersion, RealmMigration migration) { Realm.init(context); RealmConfiguration config = getRealmConfiguration(filename, schemaVersion,migration); Realm.setDefaultConfiguration(config); } private static RealmConfiguration getRealmConfiguration(@NonNull String filename, int schemaVersion, RealmMigration migration) { RealmConfiguration.Builder builder = new RealmConfiguration.Builder() .name(filename) .deleteRealmIfMigrationNeeded() .schemaVersion(schemaVersion); if(migration != null){ builder.migration(migration); } return builder.build(); } //see http://sys1yagi.hatenablog.com/entry/2015/08/14/131226 public static class AutoIncrement { public long newId(Realm realm, Class<? extends RealmModel> clazz) { return newIdWithIdName(realm, clazz, "id"); } public long newIdWithIdName(Realm realm, Class<? extends RealmModel> clazz, String idName) { Number num = realm.where(clazz).max(idName); return num == null ? 1 : num.longValue() + 1; } } }
参考情報(AutoIncrement)
保存するRealmオブジェクト
基本的に
あたりがあればいいのでその形にします。
- RealmObject ではなく RealmModelのしているのは気分的な問題。
- どちらの定義にしてもオブジェクト継承はコンパイル時にエラーになる
- 複数のRestキャッシュを作ろうとすると同じようなクラスを量産することになる
- 現状のRealmの仕様上仕方がないのかも・・*2
とりあえずGeocode用のデータを保持するRealmObject
- CachedGeo
@RealmClass public class CachedGeo implements RealmModel{ @PrimaryKey private String key; //サーバーから取得してきたときのjsonデータ private String json; //作成日 private Date createDate; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getJson() { return json; } public void setJson(String json) { this.json = json; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Date getCreateDate() { return createDate; } }
保存するJson情報<=>Gsonマッピングクラス
今更遅れてDataBinding事始め(5.5)[コピペ用] - exception think
を利用します。
アプリの仕様で、表示したい属性等がよく変わることがあるので、全情報を保持しておくほうが結果的に楽かなと思います
でちょっとここで立ち止まって考察
今回はGeocodeの例で記載していますが、公式で記載されているような記述でベタでデータ操作クラスを記述することは可能です
でもPlaceAPIとか他のRest APIも同じように作る時同じような処理を書かないといけないのは億劫。
ここでGenericsで汎用化にチャレンジしてみます
Genericsに関して
詳細は下記のページを参照。hisidama先生には何時もお世話になってお入ります(ペコリ
メンバアクセス用汎用クラス
public interface RealmIF { public String getKey(); public void setKey(String key); public String getJson(); public void setJson(String json); public void setCreateDate(Date createDate); public Date getCreateDate(); }
として
@RealmClass public class CachedGeo implements RealmModel,RealmIF{
という形に変更します
データ操作基底クラス(abstractクラス)
いつもの定形処理を汎用化記述化したのが下記のコード。
-
- impliments でも extends 記述で書く
- 複数 implimentsが有る場合は、 extends A & B と書く
ただ下記の場合はJson系のエラーは基本丸めてしまっているけど
- throwさせてDB処理自体を中断させるべきなのか
あたりは悩みどころ。
あと足りないとしたら、参考記事のようにRealmError発生時のSQlite側への縮退処理
public abstract class RealmOperate<RMODEL extends RealmModel & RealmIF,TMODEL,KEY> { private final String TAG =RealmOperate.class.getSimpleName(); private Class<RMODEL> clazzR; private Class<TMODEL> clazzT; public void init(){ clazzR = getGenericType(getClass(), RealmOperate.class, "RMODEL"); clazzT = getGenericType(getClass(), RealmOperate.class, "TMODEL"); } public RMODEL parseRealm(String key, TMODEL target) { RMODEL info = getInstance(clazzR); info.setKey(key); Gson gson = new Gson(); try{ String json = gson.toJson(target); info.setJson(json); }catch(Exception ex){ Log.e(TAG,e.getMessage(),e); return null; } return info; } public TMODEL parseObject(RMODEL info){ Gson gson = new Gson(); try{ TMODEL target = gson.fromJson(info.getJson(), clazzT); }catch(Exception ex){ Log.e(TAG,e.getMessage(),e); return null; } return target; } public boolean add(KEY arg, TMODEL in){ Realm realm = null; try { String key = getKey(arg); realm = RealmCommon.getInstance(); //キーがない場合はキーを作成する if(TextUtils.isEmpty(key)) { key = "" + new RealmCommon.AutoIncrement().newId(realm,clazzR); } RMODEL dto = parseRealm(key,in); if(dto ==null){ Log.e(TAG,ex.getMessage()); return false; } dto.setCreateDate(new Date()); realm.beginTransaction(); realm.copyToRealmOrUpdate(dto); realm.commitTransaction(); return true; } catch (RealmError ex) { Log.e(TAG,ex.getMessage()); } catch (Exception e){ Log.e(TAG,e.getMessage(),e); }finally { realm.close(); } return false; } public void remove(KEY arg){ Realm realm = null; try { String key = getKey(arg); realm = RealmCommon.getInstance(); RealmResults<RMODEL> result = realm .where(clazzR) .equalTo("key", key).findAll(); realm.beginTransaction(); Log.d(TAG,"remove:"+result.deleteAllFromRealm()); realm.commitTransaction(); } catch (RealmError ex) { Log.e(TAG,ex.getMessage()); } catch (Exception e){ Log.e(TAG,e.getMessage(),e); }finally { realm.close(); } } public void removeAll(){ Realm realm = null; try { realm = RealmCommon.getInstance(); final RealmResults<RMODEL> result = realm.where(clazzR).findAll(); realm.beginTransaction(); Log.d(TAG,"removeAll:"+result.deleteAllFromRealm()); realm.commitTransaction(); } catch (RealmError ex) { Log.e(TAG,ex.getMessage()); } catch (Exception e){ Log.e(TAG,e.getMessage(),e); }finally { realm.close(); } } public void removePassed(){ Realm realm = null; try { //現在日から1週間前のデータを消す int noOfDays = 7 * 1; Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); calendar.add(Calendar.DAY_OF_YEAR, -noOfDays); Date date = calendar.getTime(); realm = RealmCommon.getInstance(); RealmResults<RMODEL> result = realm .where(clazzR) .lessThan("createDate",date) .findAll(); realm.beginTransaction(); Log.d(TAG,"removePassed:"+result.deleteAllFromRealm()); realm.commitTransaction(); } catch (RealmError ex) { Log.e(TAG,ex.getMessage()); } catch (Exception e){ Log.e(TAG,e.getMessage(),e); }finally { realm.close(); } } public HashMap<KEY,TMODEL> getAll(){ HashMap<KEY,TMODEL> map = new HashMap<>(); RealmResults<RMODEL> result = null; Realm realm = null; try { realm = RealmCommon.getInstance(); result = realm.where(clazzR).findAll(); for (RMODEL realmInfo : result) { String key = realmInfo.getKey(); TMODEL obj = parseObject(realmInfo); if(obj != null){ map.put(cnvKey(key),obj); } } } catch (RealmError ex) { Log.e(TAG,ex.getMessage()); }catch (Exception e){ Log.e(TAG,e.getMessage(),e); }finally { if(realm != null){ realm.close(); } } return map; } //==アクセス属性を付けないとimplimentsしてくれない。。。=== abstract protected String getKey(KEY arg); abstract protected KEY cnvKey(String key); //======================================================= // //see http://d.hatena.ne.jp/Nagise/20130815/1376527213 // public static <T> T getInstance(Class<T> targetClass){ T obj = null; try { obj = targetClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return obj; } public static <T> Class<T> getGenericType(Class<?> clazz, Class<?> targetClass, String targetTypeName) { if (!targetClass.isAssignableFrom(clazz)) { throw new IllegalArgumentException("型" + clazz.getName() + "は、型" + targetClass.getName() + "を継承していません"); } Stack<Class<?>> stack = new Stack<Class<?>>(); while (!targetClass.equals(clazz.getSuperclass())) { stack.push(clazz); clazz = clazz.getSuperclass(); } return getGenericTypeImpl(clazz, targetTypeName, stack); } @SuppressWarnings("unchecked") private static <T> Class<T> getGenericTypeImpl(Class<?> clazz, String targetTypeName, Stack<Class<?>> stack) { TypeVariable<? extends Class<?>>[] superGenTypeAray = clazz .getSuperclass().getTypeParameters(); // 走査対象の型パラメータの名称(Tなど)から宣言のインデックスを取得 int index = 0; boolean existFlag = false; for (TypeVariable<? extends Class<?>> type : superGenTypeAray) { if (targetTypeName.equals(type.getName())) { existFlag = true; break; } index++; } if (!existFlag) { throw new IllegalArgumentException(targetTypeName + "に合致するジェネリクス型パラメータがみつかりません"); } // 走査対象の型パラメータが何型とされているのかを取得 ParameterizedType type = (ParameterizedType) clazz .getGenericSuperclass(); Type y = type.getActualTypeArguments()[index]; // 具象型で継承されている場合 if (y instanceof Class) { return (Class<T>) y; } // ジェネリックパラメータの場合 if (y instanceof TypeVariable) { TypeVariable<Class<?>> tv = (TypeVariable<Class<?>>) y; // 再帰して同名の型パラメータを継承階層を下りながら解決を試みる Class<?> sub = stack.pop(); return getGenericTypeImpl(sub, tv.getName(), stack); } // ジェネリック型パラメータを持つ型の場合 if (y instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) y; return (Class<T>) pt.getRawType(); } throw new IllegalArgumentException("予期せぬ型 : " + y.toString() + " (" + y.getClass() + ")"); } }
参考(Tパラメータからクラスを取得する処理)
JJUG でGenerics関係の講演をよくされている Nagise 先生のコードを利用させていただきました。
継承した実際操作クラス
public class RealmOperateGeo extends RealmOperate<CachedGeo,GeocodeResponse,LatLng>{ private final String TAG =RealmOperateGeo.class.getSimpleName(); private static RealmOperateGeo instance; private static String KEY_FORMAT="%f_%f"; public static RealmOperateGeo getInstance() { if(instance == null){ instance = new RealmOperateGeo(); instance.init(); } return instance; } //[TODO]親クラスの Tパラメータ初期化処理を呼んでやる public void init(){ super.init(); } //[TODO]Realm自体がプリミティブ型に近いObject型、Date型しかフィールドにサポートしていないため変換する // CachedGeoクラスはString型をキーとして想定している(RealmIFで関数縛りをしている) @Override protected String getKey(LatLng arg) { return String.format(KEY_FORMAT,arg.latitude,arg.longitude); } @Override protected LatLng cnvKey(String key) { String[] arr = key.split("_"); return new LatLng(Double.valueOf(arr[0]),Double.valueOf(arr[1])); } }
利用例(GeoManagerの修正)
今更遅れてDataBinding事始め(5.5)[コピペ用] - exception think
の記述をDB保存対応に実際に修正してみます
public class GeoManger{ private static GeoManger instance; private static ConcurrentHashMap<LatLng,GeocodeResponse> cashedMap = new ConcurrentHashMap<>(); public static GeoManger getInstance(){ if(instance == null){ instance = new GeoManger(); //[TODO]==== DBを読込する処理、過去データを消す処理等など ==== RealmOperateGeo.getInstance().removePassed(); HashMap<LatLng,GeocodeResponse> map = RealmOperateGeo.getInstance().getAll(); for (LatLng latLng : map.keySet()) { cashedMap.putIfAbsent(latLng,map.get(latLng)); } } return instance; } public void getGeo(final Context context,final LatLng latlng, final Callback callback){ RestUtil.geoCoding(context, latlng, new AsyncOkHttpClient.Callback() { @Override public void onSuccess(okhttp3.Response response, String content) { Gson gson = new Gson(); try { GeocodeResponse geoResponse = gson.fromJson(content, GeocodeResponse.class); if (TextUtils.equals(geoResponse.getStatus(), STATUS_OK)) { geoResponse.setContent(content); // ◎ 生Jsonを保存 cashedMap.putIfAbsent(latlng,geoResponse); //メモリ上のマップに保存 //[TODO] ====DB追加コードなど==== RealmOperateGeo.getInstance().add(latlng,geoResponse); callback.onSuccess(geoResponse); } else{ callback.onError(null); } } catch (JsonSyntaxException e) { callback.onError(e); } } }