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

【Android】SQLiteDatabaseの定形処理を作る

Android snippet

SQLiteDatabaseの処理は、SQLiteOpenHelperからDBをもらって、selectならカーソル開いて、更新ならトランザクションかけて…と、大体決まりきったものが多いです。

ちょっとしたことなら別にいいんですが、SQLiteをフル活用しようとしてDAOを作ってるとこの辺の処理を書くのが段々面倒くさくなってきます。クロージャを使ったUtilityメソッドを作ってしまいましょう。

UIスレッド以外でのDB処理

話はズレますが、UIスレッド以外からDBを操作する場合はSQLiteDatabaseが継承しているSQLiteClosableacquireReferenceメソッドを呼んでおくと安全です。

SQLiteOpenHelperから貰えるDBはすべて同一インスタンスです。仮に「UIスレッドでヘルパーを作る→バックグラウンドでそのヘルパーを使うスレッドを作る→バックグラウンドのスレッドでDBをクローズする and UIスレッド内でDBをクローズする」と言ったことをすると、SQLiteClosableが内部で所持している参照カウントの数が一致せず、例外が飛んできます。

acquireReferenceを呼ぶことでその参照カウントを明示的に増やすことが出来るので、念のため呼んでおきましょう、と言う話です。

当然参照カウントが1つ増えてるわけですから、それだけの数のDBをちゃんとクローズしないとそれはそれで怒られるんですが、SQLiteOpenHelperのcloseメソッドを呼ぶことで全ての参照カウントをリセットすることが出来ます。不要なリークを避けるためちゃんとファイナライザあたりで呼んであげましょう。

呼び出し元がUIスレッドかどうかは以下のコードで判定することが出来ます。

private static boolean isMainThread(Context cont) {
	return Thread.currentThread().equals(cont.getMainLooper().getThread());
}

トランザクション

じゃあ早速定形処理をまとめてしまいましょう。まずはトランザクション処理です。

/**
 * トランザクション処理
 * @param helper
 * @param context
 * @param act 実行内容
 */
public static void transaction(SQLiteOpenHelper helper, Context cont, V1<SQLiteDatabase> act) {
	SQLiteDatabase db = helper.getWritableDatabase();

	if(!isMainThread(cont)) db.acquireReference();

	db.beginTransaction();
	try {
		act.call(db);
		if(db.isOpen()) db.setTransactionSuccessful();
	} finally {
		if(db.isOpen()) {
			db.endTransaction();
			db.close();
		}
	}
}

実際のDB処理をactに委譲することでコーディングのミスを減らしつつ簡単にトランザクション処理を行うことが出来ます。

ただ、これだとロールバック出来ません。そんなわけで、catch節で呼ばれた時の処理も委譲出来るオーバーロードを作っておきます。

/**
 * トランザクション処理
 * @param helper
 * @param context
 * @param act 実行内容
 * @param onErrorAct catch節で実行する内容
 */
public void transaction(SQLiteOpenHelper helper, Context cont
		, V1<SQLiteDatabase> act, V2<Exception, SQLiteDatabase> onErrorAct) {

	SQLiteDatabase db = helper.getWritableDatabase();
	if(!isMainThread(cont)) db.acquireReference();

	db.beginTransaction();
	try {
		act.call(db);
		db.setTransactionSuccessful();
	} catch(Exception e) {
		onErrorAct.call(e, db);
	} finally {
		if(db.isOpen()) {
			db.endTransaction();
			db.close();
		}
	}
}

データの取得処理

先に言っておくと、私はSQLを書くのが特に苦にならない方の人間なので頻繁にrawQueryを使います。

/**
 * DBから要素を取得
 * @param query Select文
 * @param context
 * @param helper
 * @param f CursorからTを抽出するFunc
 * @return 要素が見つからない場合はnull
 */
public static <T> T get(String query, Context cont, SQLiteOpenHelper helper, R1<T, Cursor> f) {

	SQLiteDatabase db = helper.getReadableDatabase();

	if(!isMainThread(cont)) db.acquireReference();

	Cursor c = null;

	try {
		c = db.rawQuery(query, null);
	} catch(RuntimeException e) {
		db.close();
		throw e;
	}

	if(!c.moveToFirst()) {
		c.close();
		db.close();
		return null;
	}

	T obj;

	try {
		obj = f.call(c);
	} finally {
		c.close();
		db.close();
	}

	return obj;
}

/**
 * DBからすべての要素を取得
 * @param query Select文
 * @param context
 * @param helper
 * @param f CursorからTを抽出するFunc
 * @return 要素が見つからない場合は空のリスト
 */
public static <T> List<T> getList(String query, Context cont, SQLiteOpenHelper helper, R1<T, Cursor> f) {

	List<T> dataList = new ArrayList<T>();

	SQLiteDatabase db = helper.getReadableDatabase();
	if(!isMainThread(cont)) db.acquireReference();

	Cursor c = null;
	try {
		c = db.rawQuery(query, null);
	} catch(RuntimeException e) {
		db.close();
		throw e;
	}

	if(!c.moveToFirst()) {
		c.close();
		db.close()
		return dataList;
	}

	try {
		do {
			dataList.add(f.call(c));
		} while (c.moveToNext());
	} finally {
		c.close();
		db.close();
	}

	return dataList;
}

まとめ

たったこれだけですが、一つ作っておくだけでかなり簡単になります。

参考