まえおき
べつにgetReadableDatabase 直接使ってもいいんですけど、ベストプラクティスの共有という意味で捉えてください
Androidのデータベースあるある話
AndroidでSQLiteDBを使う場合には、大抵の場合はSQLiteOpenHelper を継承したクラスを定義しますね。
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) { ... }
}
で、データベースを利用するときは
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 の責務は持ちません
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クラスとして外出ししたければそれでもよいですが)
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の心配をすることなく書けた
// 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の取り扱いができるかと思います。
「そうじゃないだろ!」とか「もっといいプラクティスおれ知ってるし」っていう方はぜひコメントください!