Skip to content

Instantly share code, notes, and snippets.

@nobuoka
Last active August 6, 2018 10:49
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nobuoka/6659690 to your computer and use it in GitHub Desktop.
Save nobuoka/6659690 to your computer and use it in GitHub Desktop.
Android アプリ開発における複数スレッドでの SQLite 使用に関する調査。
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.5.+'
}
}
apply plugin: 'android'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
repositories {
mavenCentral()
}
android {
compileSdkVersion 18
buildToolsVersion "18.0.1"
defaultConfig {
minSdkVersion 7
targetSdkVersion 16
}
}
dependencies {
compile 'com.intellij:annotations:12.0'
}
package info.vividcode.android.database.sql.test;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabaseLockedException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.test.InstrumentationTestCase;
import junit.framework.AssertionFailedError;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.atomic.AtomicReference;
/**
* トランザクション中に別スレッドで DB 操作を行うテスト。
*/
public class トランザクション中に別スレッドでDB操作を行うテスト extends InstrumentationTestCase {
private static class ThreadErrorChecker implements Thread.UncaughtExceptionHandler {
public volatile Throwable error = null;
@Override public void uncaughtException(Thread thread, Throwable throwable) {
error = throwable;
}
}
private static class ItemDatabaseOpenHelper extends SQLiteOpenHelper {
static final int VERSION = 1;
static final String DB_NAME = "item_test";
static final String SQL_CREATE_ITEM_TABLE =
"CREATE TABLE item (_id INTEGER PRIMARY KEY, name TEXT UNIQUE, count INTEGER)";
public ItemDatabaseOpenHelper(Context ctx) {
super(ctx, DB_NAME, null, VERSION);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ITEM_TABLE);
}
}
/**
* Delete database.
*/
private void clearDatabase() {
Context targetCtx = getInstrumentation().getTargetContext();
targetCtx.deleteDatabase(ItemDatabaseOpenHelper.DB_NAME);
}
private static interface Prepare2DBObjCallback {
void execute(SQLiteDatabase db1, SQLiteDatabase db2);
}
private static interface Prepare1DBObjCallback {
void execute(SQLiteDatabase db);
}
/**
* テストのための `SQLiteDatabase` オブジェクトを用意する。
* @param c 生成された `SQLiteDatabase` オブジェクトを受け取るコールバック用のオブジェクト。
*/
private void SQLiteDatabaseオブジェクトを1つ用意する(@NotNull Prepare1DBObjCallback c) {
Context targetCtx = getInstrumentation().getTargetContext();
ItemDatabaseOpenHelper dbOpenHelper1 = new ItemDatabaseOpenHelper(targetCtx);
try {
SQLiteDatabase db = dbOpenHelper1.getWritableDatabase();
c.execute(db);
} finally {
dbOpenHelper1.close();
}
}
/**
* テストのための `SQLiteDatabase` オブジェクトを用意する。
* @param c 生成された `SQLiteDatabase` オブジェクトを受け取るコールバック用のオブジェクト。
*/
private void 別のSQLiteDatabaseオブジェクトを2つ用意する(@NotNull Prepare2DBObjCallback c) {
Context targetCtx = getInstrumentation().getTargetContext();
ItemDatabaseOpenHelper dbOpenHelper1 = new ItemDatabaseOpenHelper(targetCtx);
ItemDatabaseOpenHelper dbOpenHelper2 = new ItemDatabaseOpenHelper(targetCtx);
try {
SQLiteDatabase db1 = dbOpenHelper1.getWritableDatabase();
SQLiteDatabase db2 = dbOpenHelper2.getReadableDatabase();
c.execute(db1, db2);
} finally {
dbOpenHelper1.close();
dbOpenHelper2.close();
}
}
/**
* SQLite で DB ロックを取得できなかったことにより発生する例外クラス。
* API level 11 以降は専用のクラスが作られた。
*/
private static final Class<? extends SQLiteException> SQLITE_DB_LOCKED_EXCEPTION_CLASS =
(Build.VERSION.SDK_INT >= 11 ? SQLiteDatabaseLockedException.class : SQLiteException.class);
/**
* API level によって? 端末によって? も違うみたい。
* (新しい API level だと 2000 ms で大丈夫だが、古い API level だと 2000 ms だと例外が出るっぽい)
*/
private static final int 例外が発生することなくロック解除を待てる時間InMs = 1000;
/**
* API level によって? 端末によって? も違うみたい。
* 試してみた条件の中では全て 4000 ms ぐらい経つと例外発生した。
*/
private static final int ロック解除を待てずに例外が発生する時間InMs = 4000;
private static class ThreadForTest extends Thread {
private final ThreadErrorChecker errorChecker;
public ThreadForTest(Runnable process) {
super(process);
errorChecker = new ThreadErrorChecker();
this.setUncaughtExceptionHandler(errorChecker);
}
public void assertTerminatedWithoutException() {
assertEquals("スレッドは終了している", Thread.State.TERMINATED, this.getState());
if (errorChecker.error instanceof AssertionFailedError) {
throw ((AssertionFailedError) errorChecker.error);
} else {
// スレッドでチェックされない例外が投げられていないなら `null` になっている。
assertNull(errorChecker.error + " が `null` である", errorChecker.error);
}
}
}
private static void assertException(Class<? extends Throwable> expectedClass, Runnable testCode) {
Throwable err = null;
try {
testCode.run();
} catch (Throwable e) {
err = e;
}
assertNotNull(err);
assertTrue(expectedClass.isInstance(err));
}
private static abstract class Abstractトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト {
private static enum TransactionState {
BEFORE_TRANSACTION, IN_TRANSACTION, AFTER_TRANSACTION
}
private final AtomicReference<TransactionState> transactionState =
new AtomicReference<TransactionState>(TransactionState.BEFORE_TRANSACTION);
protected abstract int トランザクション中の待ち時間inMs();
protected abstract int トランザクション後の待ち時間inMs();
protected abstract void 別スレッドで実行する処理();
protected abstract void beginTransaction(SQLiteDatabase db);
protected final void assertInTransaction(String msg) {
synchronized (transactionState) {
assertEquals(msg, TransactionState.IN_TRANSACTION, transactionState.get());
}
}
protected final void assertTransactionEnded(String msg) {
synchronized (transactionState) {
assertEquals(msg, TransactionState.AFTER_TRANSACTION, transactionState.get());
}
}
public final void execTest(SQLiteDatabase db) {
ThreadForTest t = new ThreadForTest(new Runnable() {
@Override public void run() {
別スレッドで実行する処理();
}
});
// 挿入するデータ
final ContentValues row = new ContentValues();
long rowId;
synchronized (transactionState) {
beginTransaction(db);
transactionState.set(TransactionState.IN_TRANSACTION);
}
try {
// 1 個めの項目挿入
row.put("name", "arrow");
row.put("count", 5);
rowId = db.insertOrThrow("item", null, row);
assertTrue("挿入に失敗していない", -1 != rowId);
// 別スレッドの処理を開始
t.start();
try {
Thread.sleep(トランザクション中の待ち時間inMs());
} catch (InterruptedException e) { e.printStackTrace(); }
// 2 個めの項目挿入
row.put("name", "bean");
row.put("count", 5);
rowId = db.insertOrThrow("item", null, row);
assertTrue("挿入に失敗していない", -1 != rowId);
db.setTransactionSuccessful();
} finally {
synchronized (transactionState) {
db.endTransaction();
transactionState.set(TransactionState.AFTER_TRANSACTION);
}
}
try {
t.join(トランザクション後の待ち時間inMs());
} catch (InterruptedException e) { e.printStackTrace(); }
t.assertTerminatedWithoutException();
}
}
private static abstract class トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト
extends Abstractトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト {
@Override
protected final void beginTransaction(SQLiteDatabase db) {
db.beginTransaction();
}
}
@TargetApi(11)
private static abstract class NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト
extends Abstractトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト {
@Override
protected final void beginTransaction(SQLiteDatabase db) {
db.beginTransactionNonExclusive();
}
}
public void test_トランザクション中に別スレッドでSQLiteDatabaseオブジェクトを生成する() {
// 別のスレッドによるロック解放待ちが指定秒数 (2500 ms?; このテストでは
// `ロック解除を待てずに例外が発生する時間InMs` だけ 待っている) を超えると
// `SQLiteException` が発生する。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
Context targetCtx = getInstrumentation().getTargetContext();
final ItemDatabaseOpenHelper dbOpenHelper = new ItemDatabaseOpenHelper(targetCtx);
try {
assertException(SQLITE_DB_LOCKED_EXCEPTION_CLASS, new Runnable() {
@Override public void run() {
dbOpenHelper.getWritableDatabase();
}
});
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
} finally {
dbOpenHelper.close();
}
}
}.execTest(db);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
Context targetCtx = getInstrumentation().getTargetContext();
final ItemDatabaseOpenHelper dbOpenHelper = new ItemDatabaseOpenHelper(targetCtx);
try {
assertNotNull(dbOpenHelper.getWritableDatabase());
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
} finally {
dbOpenHelper.close();
}
}
}.execTest(db);
}
});
// `getReadableDatabase` メソッドの場合は `getWritableDatabase` メソッドよりもロック解除
// を待てる時間が長いっぽいけど、その時間は API level によっても違ってるみたいでよくわからない。
}
public void test_NonExclusiveトランザクション中に別スレッドでSQLiteDatabaseオブジェクトを生成する() {
// API level 11 より前は `beginTransactionNonExclusive` がなかったのでテストはしない
if (Build.VERSION.SDK_INT < 11) return;
// 別の `SQLiteDatabase` オブジェクトを使用する場合、別のスレッドによるロック解放待ちが
// 指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs` だけ待っている)
// を超えると `SQLiteException` が発生する。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override
public void execute(final SQLiteDatabase db) {
new NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override
protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override
protected int トランザクション後の待ち時間inMs() {
return 1000;
}
@Override
protected void 別スレッドで実行する処理() {
Context targetCtx = getInstrumentation().getTargetContext();
final ItemDatabaseOpenHelper dbOpenHelper = new ItemDatabaseOpenHelper(targetCtx);
try {
assertNotNull(dbOpenHelper.getWritableDatabase());
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
} finally {
dbOpenHelper.close();
}
}
}.execTest(db);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
new NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
Context targetCtx = getInstrumentation().getTargetContext();
final ItemDatabaseOpenHelper dbOpenHelper = new ItemDatabaseOpenHelper(targetCtx);
try {
assertNotNull(dbOpenHelper.getWritableDatabase());
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
} finally {
dbOpenHelper.close();
}
}
}.execTest(db);
}
});
}
public void test_トランザクション中に別スレッドでトランザクションを開始する() {
// 別の `SQLiteDatabase` オブジェクトを使用する場合、別のスレッドによるロック解放待ちが
// 指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs` だけ待っている)
// を超えると `SQLiteException` が発生する。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
assertException(SQLITE_DB_LOCKED_EXCEPTION_CLASS, new Runnable() {
@Override
public void run() {
db2.beginTransaction();
}
});
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
}
}.execTest(db1);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
db2.beginTransaction();
try {
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
} finally {
db2.endTransaction();
}
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使用する場合、別のスレッドによる
// ロック解放待ちは指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs`
// だけ待っている) を超えても例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
db.beginTransaction();
try {
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
} finally {
db.endTransaction();
}
}
}.execTest(db);
}
});
}
public void test_トランザクション中に別スレッドで挿入処理を行う() {
/** テストで挿入する行のデータ */
final ContentValues rowOfCup = new ContentValues();
rowOfCup.put("name", "cup");
rowOfCup.put("count", 5);
// 別の `SQLiteDatabase` オブジェクトを使用する場合、別のスレッドによるロック解放待ちが
// 指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs` だけ待っている)
// を超えると `SQLiteException` が発生する。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
assertException(SQLITE_DB_LOCKED_EXCEPTION_CLASS, new Runnable() {
@Override public void run() {
db2.insertOrThrow("item", null, rowOfCup);
}
});
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
}
}.execTest(db1);
}
});
// `insert` を使った場合は例外が発生しないが、どちらにせよ挿入に失敗する。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
long rowId = db2.insert("item", null, rowOfCup);
assertEquals("挿入に失敗するので -1 が返る", -1, rowId);
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
}
}.execTest(db1);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
long rowId = db2.insertOrThrow("item", null, rowOfCup);
assertTrue("挿入に失敗していない", -1 != rowId);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合は待ち時間が
// `ロック解除を待てずに例外が発生する時間InMs` を超えても例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
long rowId = db.insertOrThrow("item", null, rowOfCup);
assertTrue("挿入に失敗していない", -1 != rowId);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db);
}
});
}
public void test_トランザクション中に別スレッドで削除処理を行う() {
/** テストで挿入、削除する行のデータ */
final ContentValues rowOfBottle = new ContentValues();
rowOfBottle.put("name", "bottle");
rowOfBottle.put("count", 5);
// 別の `SQLiteDatabase` オブジェクトを使用する場合、別のスレッドによるロック解放待ちが
// 指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs` だけ待っている)
// を超えると `SQLiteException` が発生する。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
long rowId = db1.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
assertException(SQLITE_DB_LOCKED_EXCEPTION_CLASS, new Runnable() {
@Override
public void run() {
db2.delete("item", "name = 'bottle'", null);
}
});
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
}
}.execTest(db1);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
long rowId = db1.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
int numDeletedRows = db2.delete("item", "name = 'bottle'", null);
assertEquals("1 行の削除", 1, numDeletedRows);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合は待ち時間が
// `ロック解除を待てずに例外が発生する時間InMs` を超えても例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
long rowId = db.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
int numDeletedRows = db.delete("item", "name = 'bottle'", null);
assertEquals("1 行の削除", 1, numDeletedRows);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db);
}
});
}
public void test_トランザクション中に別スレッドで更新処理を行う() {
/** テストで挿入、更新する行のデータ */
final ContentValues rowOfBottle = new ContentValues();
rowOfBottle.put("name", "bottle");
rowOfBottle.put("count", 5);
/** 更新に使うためのインスタンス */
final ContentValues rowOfBottleToUpdate = new ContentValues();
rowOfBottleToUpdate.put("count", 3);
// 別の `SQLiteDatabase` オブジェクトを使用する場合、別のスレッドによるロック解放待ちが
// 指定秒数 (2500 ms?; このテストでは `ロック解除を待てずに例外が発生する時間InMs` だけ待っている)
// を超えると `SQLiteException` が発生する。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
long rowId = db1.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
assertException(SQLITE_DB_LOCKED_EXCEPTION_CLASS, new Runnable() {
@Override public void run() {
db2.update("item", rowOfBottleToUpdate, "name = 'bottle'", null);
}
});
assertInTransaction("トランザクション (このスレッドではないスレッドで張られている) はまだ終わっていない");
}
}.execTest(db1);
}
});
// 待ち時間が `例外が発生することなくロック解除を待てる時間InMs` 程度なら例外は発生しない。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
long rowId = db1.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return 例外が発生することなくロック解除を待てる時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
int numUpdatedRows = db2.update("item", rowOfBottleToUpdate, "name = 'bottle'", null);
assertEquals("1 行の更新", 1, numUpdatedRows);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合は待ち時間が
// `ロック解除を待てずに例外が発生する時間InMs` を超えても例外は発生しない。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
long rowId = db.insertOrThrow("item", null, rowOfBottle);
assertTrue("挿入に失敗していない", -1 != rowId);
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() {
return ロック解除を待てずに例外が発生する時間InMs;
}
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
int numUpdatedRows = db.update("item", rowOfBottleToUpdate, "name = 'bottle'", null);
assertEquals("1 行の更新", 1, numUpdatedRows);
assertTransactionEnded("このスレッドじゃないスレッドで張られているトランザクションは終わっている");
}
}.execTest(db);
}
});
}
public void test_トランザクション中に別スレッドでクエリ発行を行う() {
// トランザクション開始や行挿入処理とは違い、クエリ発行では待ち時間が 5000 ms でも
// 別スレッドでのトランザクション終了を待つ。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() { return 5000; }
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
final Cursor c = db2.query("item", new String[]{"name"}, null, null, null, null, null);
try {
assertInTransaction("別スレッドでトランザクションが張られていても `query` メソッド呼び出しは返ってくる");
assertEquals(2, c.getCount()); // `getCount` でトランザクション終了を待つ
} finally {
c.close();
}
assertTransactionEnded("このスレッドとは別のスレッドで張られているトランザクションは終わっている");
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合もロック取得を待つ。
// ただし、`query` メソッドで待つところが異なる `SQLiteDatabase` を使う場合とは異なる。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override
public void execute(final SQLiteDatabase db) {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override
protected int トランザクション中の待ち時間inMs() {
return 5000;
}
@Override
protected int トランザクション後の待ち時間inMs() {
return 1000;
}
@Override
protected void 別スレッドで実行する処理() {
// `query` メソッドでトランザクション終了を待つ
final Cursor c = db.query("item", new String[]{"name"}, null, null, null, null, null);
try {
assertTransactionEnded("このスレッドとは別のスレッドで張られているトランザクションは終わっている");
assertEquals(2, c.getCount());
} finally {
c.close();
}
}
}.execTest(db);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合もロック取得を待つ。
// トランザクション開始前に `query` メソッドを呼び出した場合は `Cursor` への操作で待つ。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
final Cursor c = db.query("item", new String[]{"name"}, null, null, null, null, null);
try {
new トランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() { return 5000; }
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
assertEquals(2, c.getCount());
assertTransactionEnded("このスレッドとは別のスレッドで張られているトランザクションは終わっている");
}
}.execTest(db);
} finally {
c.close();
}
}
});
}
@TargetApi(11)
public void test_NonExclusiveトランザクション中に別スレッドでクエリ発行を行う() {
// API level 11 より前は `beginTransactionNonExclusive` がなかったのでテストはしない
if (Build.VERSION.SDK_INT < 11) return;
// 異なる `SQLiteDatabase` オブジェクトを使う場合は、別スレッドで non exclusive の
// トランザクションが張られていてもトランザクション終了を待たずにクエリ発行が行われる。
clearDatabase();
別のSQLiteDatabaseオブジェクトを2つ用意する(new Prepare2DBObjCallback() {
@Override public void execute(SQLiteDatabase db1, final SQLiteDatabase db2) {
new NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() { return 5000; }
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
final Cursor c = db2.query("item", new String[]{"name"}, null, null, null, null, null);
try {
assertEquals(0, c.getCount()); // `getCount` でトランザクション終了を待つ
} finally {
c.close();
}
assertInTransaction("別スレッドでトランザクションが張られていても `getCount` メソッド呼び出しは返ってくる");
}
}.execTest(db1);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合は、別スレッドでのトランザクションが
// non exclusive であってもトランザクション終了を待つ。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override
public void execute(final SQLiteDatabase db) {
new NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override
protected int トランザクション中の待ち時間inMs() {
return 5000;
}
@Override
protected int トランザクション後の待ち時間inMs() {
return 1000;
}
@Override
protected void 別スレッドで実行する処理() {
// Non exclusive であっても、同一 `SQLiteDatabase` オブジェクトを使っている場合は `query` メソッドでトランザクション終了を待つ
final Cursor c = db.query("item", new String[]{"name"}, null, null, null, null, null);
try {
assertTransactionEnded("このスレッドとは別のスレッドで張られているトランザクションは終わっている");
assertEquals(2, c.getCount());
} finally {
c.close();
}
}
}.execTest(db);
}
});
// 同一の `SQLiteDatabase` オブジェクトを複数スレッドで使う場合は、別スレッドでのトランザクションが
// non exclusive であってもトランザクション終了を待つ。
clearDatabase();
SQLiteDatabaseオブジェクトを1つ用意する(new Prepare1DBObjCallback() {
@Override public void execute(final SQLiteDatabase db) {
final Cursor c = db.query("item", new String[]{"name"}, null, null, null, null, null);
try {
new NonExclusiveトランザクションを張って2項目を挿入する間に別スレッドで処理を実行するテスト() {
@Override protected int トランザクション中の待ち時間inMs() { return 5000; }
@Override protected int トランザクション後の待ち時間inMs() { return 1000; }
@Override protected void 別スレッドで実行する処理() {
// `query` メソッド呼び出しがトランザクション開始前の場合は `Cursor` 操作時に
// トランザクション終了を待つ。
assertEquals(2, c.getCount());
assertTransactionEnded("このスレッドとは別のスレッドで張られているトランザクションは終わっている");
}
}.execTest(db);
} finally {
c.close();
}
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment