LoginSignup
57
51

More than 5 years have passed since last update.

SQLiteOpenHelper を使うときには、getReadableDatabase/getWritableDatabaseを直接使わない!

Last updated at Posted at 2015-08-20

まえおき

べつにgetReadableDatabase 直接使ってもいいんですけど、ベストプラクティスの共有という意味で捉えてください
 

Androidのデータベースあるある話

AndroidでSQLiteDBを使う場合には、大抵の場合はSQLiteOpenHelper を継承したクラスを定義しますね。

MyDatabaseHelper
public class MyDatabaseHelper extends SQLiteOpenHelper{
    private static final int DB_VERSION = 1;

    public MyDatabaseHelper (Context context) {
        super(context, "mydata.db", 0, DB_VERSION);
    }


    @Override
    public void onCreate (SQLiteDatabase db) { ... }

    @Override
    public void onUpgrade (SQLiteDatabase db, int oldVersion, int newVersion) { ... }

    @Override
    public void onDowngrade (SQLiteDatabase db, int oldVersion, int newVersion) { ... }

}

で、データベースを利用するときは

read例
SQLiteDatabase db = new MyDatabaseHelper(context).getReadableDatabase();
try{
    Cursor c = db.query(...);

     :
    c.close();
}
finally{
    db.close();
}

こんなかんじのコードを書くわけですが、CursorやSQLiteDatabase のclose() を呼ぶのを忘れることがよくあります。
また、 内部のDBハンドルのライフサイクルの理解が浅いエンジニアが、closeを呼ぶタイミングを間違い、謎のエラーに悩まされる ということもよくあります。

「closeをしなきゃ」と思わなくてもいいコードへ

やることは2つです。
1つは、(RailsでいうActiveRecordてきな)モデルを定義すること。
もう1つは、SQLiteDatabase のインスタンス取得/破棄をMyDatabaseHelper側に共通処理化してしまうこと。

モデル定義

Cursor のclose() を意識しなくてもいいようにやります。
SQLiteDatabase のopen/close の責務は持ちません

Userモデル
public class User {
    private static final String TABLE_NAME = "user";

    public int _id;
    public String name;
    public int age;

    private User(){
        _id = -1;
    }   

    private static User createUsingCursor(Cursor c, int offset) {
        /* open済みのCursorからインスタンスを生成するためのstaticメソッド。Cursorのcloseの責務は持たない */
        User u = new User();
        u._id = c.getInt(offset);
        u.name = c.getString(offset+1);
        u.aget = c.getInt(offset+2);
        return u;
    }

    public static List<User> list(SQLiteDatabase db, String selection, String orderBy) {
        /* open済みのdbからモデルのインスタンスを生成するためのstaticメソッド。dbのcloseの責務は持たない */
        /* Cursor のopen/close は責任持って "気をつけて" 行う */

        ArrayList<User> userlist = new ArrayList<>();

        Cursor c = db.query(TABLE_NAME, null, selection, null, null, null, orderBy);
        if (c!=null) {
            try {
                while (c.moveToNext()) {    
                    userlist.add(createUsingCursor(c, 0));
                }
            }
            finally {
                c.close();
            }
        }

        return userlist;
    }

    public static User get(SQLiteDatabase db, int id) {
        /* open済みのdbからモデルのインスタンスを生成するためのstaticメソッド。dbのcloseの責務は持たない */
        /* Cursor のopen/close は責任持って "気をつけて" 行う */

        Cursor c = db.query(TABLE_NAME, null, "_id="+id , null, null, null, orderBy);

        User u = null;
        if (c!=null) {
            try {
                u = createUsingCursor(c, 0);
            }
            finally {
                c.close();
            }
        }

        return u;
    }

    // 以下も必要に応じて同様に定義   
    public void put(SQLiteDatabase db) { ... }

    public void delete(SQLiteDatabase db) { ... }

    public static void createTable(SQLiteDatabase db) { ... }

    public static void updateTable(SQLiteDatabase db, int oldVersion, int newVersion) { ... }

    public static void dropTable(SQLiteDatabase db) { ... }
}

SQLiteDatabaseインスタンスを生成/破棄の処理共通化

SQLiteDatabaseのcloseを意識しなくてもいいように、やります。
MyDatabaseHelperに少し処理を追加します。(Utilクラスとして外出ししたければそれでもよいですが)

MyDatabaseHelper
public class MyDatabaseHelper extends SQLiteOpenHelper{
    :
    :
  public interface DBProcess<T> {
        T process(SQLiteDatabase db);
  }
  
    public static <T> T read(Context context, DBProcess handler) {
        T obj = null;

        SQLiteDatabase db = new MyDatabaseHelper(context).getReadableDatabase();
        try {
            obj = handler.process(db);
        }
        finally {
            db.close();
        }
        return obj; 
    }

    public static void write(Context context, DBProcess<Object> handler) {
        SQLiteDatabase db = new MyDatabaseHelper(context).getReadableDatabase();
        try {
            handler.process(db);
        }
        finally {
            db.close();
        }
    }

closeの心配をすることなく書けた

read例
    // id=123 のUserを取得する
    final int targetID = 123;
    User u = MyDatabaseHelper.read(context, new MyDatabaseHelper.DBProcess<User>() {
        public User process(SQLiteDatabase db) {
            return User.get(db, targetID);
        }
    });

データベースのハンドルのライフサイクルをコントロールしつつ処理をする必要があるようなケースを除けば、
だいたいこんなかんじの書き方で済ませられることが多いです。
「書く量が多くね?」と思うかもしれませんが、Android Studioがいい感じに補完してくれるので、実はわりと楽です。

process() の中と外とでやりとりできる値はfinal指定する必要がある という制約があるため、
無意識のうちに、データベースにかかわる処理と、そうじゃない処理の書き分けもできるでしょう。

今回は、説明簡略化のためにトランザクション無し版のwriteしか定義していませんが、トランザクションあり版のwriteメソッドを別途つくれば、beginTransaction/endTransaction あたりも意識しないで書けるでしょう。
 

おわりに

DBハンドルの取扱い責務の明確化という観点においては、わりといいプラクティスなのではないかなぁと個人的に思っています。
「AndroidでDBさわったことない人がプロジェクトに入ってきた!」という場合でも、こういう仕組みが用意されていると、安心してDBの取り扱いができるかと思います。
 
 
「そうじゃないだろ!」とか「もっといいプラクティスおれ知ってるし」っていう方はぜひコメントください!

57
51
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
51