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

通信キャッシュのDB処理設計の考察

android realm

はじめに

今更遅れて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オブジェクト

基本的に

  • キー
    • 情報を参照するためのユニークキー*1
  • サーバーからのJson
  • 受信日
    • 古いデータキャッシュを消すため

あたりがあればいいのでその形にします。

  • RealmObject ではなく RealmModelのしているのは気分的な問題。
  • どちらの定義にしてもオブジェクト継承はコンパイル時にエラーになる
    • 複数のRestキャッシュを作ろうとすると同じようなクラスを量産することになる
    • 現状のRealmの仕様上仕方がないのかも・・*2

とりあえずGeofence用のデータを保持する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クラス)

いつもの定形処理を汎用化記述化したのが下記のコード。

  • Generics

    • 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パラメータからクラスを取得する処理)

JJUGGenerics関係の講演をよくされている 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);
                }
            }


}

*1:Realmって重複キーinsertは不許可だった気がする

*2:実装的に美しくないですが・・・