Spica*

プログラミングの話。

Cursor->定義したModel(Bean)への変換を楽にする

Androidで、よくSQLiteデータベースからデータを取得するとき、DAOクラス内でCursorオブジェクトから別のデータ保持用のModel(Bean)クラスへ変換してから、実際のプログラムの方へ返したいことが多いのですね。

DAOって何かって言うと、データベースから取得したデータを、メインのプログラム(Activityやロジックなど)側で扱いやすいように変換する役割を持つクラスのことです。中に実装するメソッドは、例えばCursorからデータをArrayList<T>に起こしたりとかするものを書きます。また、メインのプログラム→データベースへデータを格納する場合にも使います。

で、Androidって、データベースからデータを取得するとき、以下の様な感じでCursorのループを、回しながらデータを取得しなきゃいけないと思うんです。

    public Favorite getFavoriteByIdOld(long id) {
        Uri uri = ContentUris.appendId(Tables.Favorites.getContentUri().buildUpon(), id).build();
        Cursor c = getContext().getContentResolver().query(uri, null, null, null, Tables.Favorites.CREATED + " DESC");
        try {
            if (c.moveToFirst()) {
                Favorite favorite = new Favorite();
                favorite.setId(c.getLong(c.getColumnIndex(Tables.Favorites._ID)));
                favorite.setUrl(c.getString(c.getColumnIndex(Tables.Favorites.URL.toString())));
                favorite.setTitle(c.getString(c.getColumnIndex(Tables.Favorites.TITLE.toString())));
                favorite.setCreated(c.getLong(c.getColumnIndex(Tables.Favorites.CREATED.toString())));
                favorite.setModified(c.getLong(c.getColumnIndex(Tables.Favorites.MODIFIED.toString())));

                return favorite;
            }
        } finally {
            c.close();
        }

        return null;
    }

同じコード書いたことある人はわかると思うんですが、これがまたすごい行数食うんですよね。条件によってこういうメソッドいちいち作らないといけないので、DAOが同じようなコードで膨らむわ膨らむわ…

ということで、

  • 型安全であること
  • カーソルを処理後にクローズしてくれること
  • 検索条件を自由に指定できること

を条件にうまく共通化できないかなと思ってました。 その答えが最近やっと出てきたのでメモ。

まず、Daoクラスのベースとなるクラスを作っておく。

public class BaseDao {

    private final Context mContext;

    public BaseDao(Context context) {
        mContext = context.getApplicationContext();
    }

    /**
     * {@link android.database.Cursor} を、指定した引数の {@link java.util.ArrayList} へ変換する。
     * 
     * @param openedCursor
     * @param klass
     *            {@link java.util.ArrayList} 内のクラス。クラスには {@link BaseDao.Beanable}
     *            を実装し、さらにデフォルトコンストラクタを用意しておくことが必須。
     * @return
     */
    public <T extends Beanable> ArrayList<T> toArrayList(Cursor openedCursor, Class<T> klass) {
        ArrayList<T> list = new ArrayList<T>();
        if (openedCursor != null) {
            try {
                if (openedCursor.moveToFirst()) {
                    do {
                        try {
                            T instance = klass.newInstance();
                            instance.setCursor(openedCursor);
                            list.add(instance);
                        } catch (InstantiationException e) {
                            throw new RuntimeException("デフォルトコンストラクタ、ねぇから!", e);
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException("アクセス修飾子間違ってるから!!", e);
                        }
                    } while (openedCursor.moveToNext());
                }
            } finally {
                openedCursor.close();
            }
        }

        return list;
    }

    protected Context getContext() {
        return mContext;
    }

    public interface Beanable {
        public void setCursor(Cursor c);
    }
}

次に、Cursorのデータを詰めるクラスを作る。

このとき、型安全を実現するために、Cursorのデータを取り出すメソッドを実装するよう、インターフェースを実装させる。さらに、デフォルトコンストラクタを作っておく

public class Favorite implements Beanable {
    private String url;
    private String title;

    public Favorite() {
    }

    @Override
    public void setCursor(Cursor c) {
        int i;
        i = c.getColumnIndex(Tables.Favorites.URL.toString());
        if (i != -1) {
            setUrl(c.getString(i));
        }
        i = c.getColumnIndex(Tables.Favorites.TITLE.toString());
        if (i != -1) {
            setTitle(c.getString(i));
        }
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

}

あとは、Daoのクラスを実装して、取得するメソッドを書く!

public class Dao extends BaseDao {

    public Dao(Context context) {
        super(context);
    }

    public ArrayList<Favorite> getFavoriteList() {
        Uri uri = Tables.Favorites.getContentUri();
        ArrayList<Favorite> modelList = toArrayList(getContext().getContentResolver().query(
                uri, null, null, null, Tables.Favorites.CREATED + " DESC"), Tables.Favorite.class);

        return modelList;
    }

    // ...etc...
}

上記のデメリットは、

  • Beanableを実装したクラス作んなきゃダメ
    • 例えば、「特定の1つのフィールドだけを取り出したいだけ」という場合に使えない
    • 上記の反論:「そこまで多いコードではないし、その場合この関数使わずに直接書いても良い。この関数のメリットは、CursorのデータをBeanに落としこむ時に一番効果的である」
  • 途中に処理を挟めない
    • Cursorからデータ取り出した時すぐに、計算を行い、結果をBeanに格納したい
    • 上記の反論:「Beanableで実装したメソッドで行うか、ループを抜けた後で再度ループを回すと良い。二度手間になるとか、パフォーマンスが気になる場合があるのかもしれないけれど、そもそもSQLiteへのアクセスにPF求めるのが設計としてまずいので、その辺り求めるのであればAndroid アプリ開発における SQLite のロックとマルチスレッドの話 - ひだまりソケットは壊れないを参考に、読み取り用のデータベースコネクションを張って死亡しないようにサブスレッドで処理すべき」

あたりかな…。

まぁ何が言いたいかというとジェネリクスかわいいよジェネリクス