2012/07/31

Android:bulkInsertとapplyBatchのアトミック性を保証する

●blukInsert

ContentResolverやContentProviderClientにあるbulkInsertはアトミックな操作ではありません。
トランザクションの開始なしに、連続してinsertするAPIです。
ContentResolver.bulkInsert

・android.content.ContentProvider.bulkInsert(Uri, ContentValues[])
/**
 * Override this to handle requests to insert a set of new rows, or the
 * default implementation will iterate over the values and call
 * {@link #insert} on each of them.
 * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
 * after inserting.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param uri The content:// URI of the insertion request.
 * @param values An array of sets of column_name/value pairs to add to the database.
 * @return The number of values that were inserted.
 */
public int bulkInsert(Uri uri, ContentValues[] values) {
    int numValues = values.length;
    for (int i = 0; i < numValues; i++) {
        insert(uri, values[i]);
    }
    return numValues;
}

bulkInsertのアトミック性を保証したい場合、独自のプロバイダでbulkInsertをオーバーライドします。
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        SQLiteStatement insertStmt = db
                .compileStatement("INSERT INTO test "
                        + "(name) VALUES (?);");
        for (ContentValues value : values) {
            insertStmt.bindString(1, value.getAsString("name"));
            insertStmt.executeInsert();
        }
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
    ...
これでbulkInsertのアトミック性が保証されます。


●applyBatch

blukInsertと同じく。
ContentResolverやContentProviderClientにあるapplyBatchもアトミックな操作ではありません。
まとめてContentProviderOperationを実行するためのAPIで、トランザクションは開始されません。
ContentResolver.applyBatch

・android.content.ContentProvider.applyBatch(ArrayList<ContentProviderOperation>)
/**
 * Override this to handle requests to perform a batch of operations, or the
 * default implementation will iterate over the operations and call
 * {@link ContentProviderOperation#apply} on each of them.
 * If all calls to {@link ContentProviderOperation#apply} succeed
 * then a {@link ContentProviderResult} array with as many
 * elements as there were operations will be returned.  If any of the calls
 * fail, it is up to the implementation how many of the others take effect.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param operations the operations to apply
 * @return the results of the applications
 * @throws OperationApplicationException thrown if any operation fails.
 * @see ContentProviderOperation#apply
 */
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    final int numOperations = operations.size();
    final ContentProviderResult[] results = new ContentProviderResult[numOperations];
    for (int i = 0; i < numOperations; i++) {
        results[i] = operations.get(i).apply(this, results, i);
    }
    return results;
}

applyBatchのアトミック性を保証したい場合、独自のプロバイダでapplyBatchをオーバーライドします。
@Override
public ContentProviderResult[] applyBatch(
        ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        ContentProviderResult[] result = super.applyBatch(operations);
        db.setTransactionSuccessful();
        return result;
    } finally {
        db.endTransaction();
    }
}
これでapplyBatchのアトミック性が保証されます。

以上です。

2012/07/30

Android:SQLiteのロックとTransaction Immediate/Exclusive


!この記事は古くなっています. 更新版は下記をご覧ください.!
  Android: SQLite3 LockとTransaction Immediate/Exclusive
  http://yuki312.blogspot.jp/2014/10/android-sqlite3-locktransaction.html


SQLiteのロック機構と、Transactionで使用できるロック2種の選択基準についての考察。

●SQLiteのロック

SQLiteのロック単位は"データベース単位"。
このため、ロックを1つ取得すると同データベース上にある全てのテーブルに影響がある。

Oracle等の巨大なDBMSでは"行ロック"なんてものがあったりして細かくロックを制御できるが、軽量なSQLiteは"データベース単位"でのロックのみサポートしている。
そのため、長い時間ロックし続けると他テーブルになかなかアクセスできない状態になる。
全く問題にならないケースもあるが、"データベースを分割するかしないか"の判断基準の1つにはなりそう。


●Transaction Immediate

Transactionで指定できるロック種別の1つがImmediate。
Immediateロックモードのポイントは下記
・ロック中、他ユーザはデータの読み取りはできても書き込みはできない
長所は、ロック中でも他ユーザがデータを読めるため"ロック解除されるまで待たなくて良い"こと。
逆に"読めてしまうことによる弊害"がある場合はExclusiveロックの使用を検討する。

Immediateロックではマズいケースは下記のような場合かな?あまり良い例が思いつかない。
  1.  アプリの設定値を保持したテーブルがある
  2.  アプリの設定値は他モジュールから参照される
  3.  アプリの設定値は可能な限り最新値を返す
  4.  アプリの設定値の更新には多少時間が掛かる
アプリの設定値を更新する必要があるシーン(つまりDBテーブルの値が古い状態)で、設定値の更新を試みる。
ただし、設定値の更新は多少時間がかかる(4)。
Immediateロックでは、更新中(ロック中)にデータベースを参照されると古い値が返されるのでNG(3)。
そのため、参照もできなくするExclusiveロックを掛ける。


●Transaction Exclusive

Transactionで指定できるもう1つのロック種別がExclusive。
Exclusiveロックモードのポイントは下記
・ロック中、他ユーザはデータの読み書きができない
長所は、"ロック中に読み書きできるのは自ユーザのみ"なこと。
逆に"他ユーザの読み取りを待たせてしまうことによる弊害"がある場合はImmediateロックの使用を検討する。

Exclusiveロックだとマズいケースは下記のような場合かな?
  1.  とあるデータAは多数のユーザから頻繁に読み取りアクセスされる
  2.  とあるデータAを読み取るアプリは高い応答性が求められる
  3.  データ更新中に並行してデータ参照されても平気
Exclusiveロックを取得している間、他ユーザは同データベース内にあるテーブルや行/列のデータを参照できない。
そのため、Exclusiveロックが長くなればなるほど、データを参照したいアプリの応答性を損なう原因となり兼ねない(2)。
(3)があてはまるならImmediateロックで問題ないかな?

# 上記2つのケースがImmediate/Exclusiveロックの考え方として正しいのか自信なし。
# もっと良いケースがあれば後日修正します。


●ソースコード

AndroidでのTransactionはSQLiteDataBaseクラスを使用します。

基本的な使い方は割愛。下記サイトで詳しく取り上げられています。
AABlog:Androidアプリ開発 SQLiteデータベースを使用する(トランザクション)

AndroidでTransactionのロック種別を指定するにはSQLiteDataBaseクラスを使用します。
使用するのは下記のAPI
※beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveはAPI Level11(Honeycomb)で追加されたAPIです。

beginTransaction/beginTransactionWithListenerで開始されたトランザクションは「Exclusiveモード」で、
beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveで開始されたトランザクションは「Immediateモード」でデータベースをロックします。
// 「Exclusiveモード」でロック
db.beginTransaction();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

// 「Immediateモード」でロック
db.beginTransactionNonExclusive();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
簡単ですね。

●SQLiteDatabaseLockedException

ロックされているデータベースに対してSQLiteDatabase経由でクエリを実行した場合、、、

・SQLiteDatabase.query
ロックが取得できるまで待ちます。
API Lv16(JellyBean)以降はCancellationSignalを使ってクエリをキャンセルすることが出来ます。
ContentResolver.query()
CancellationSignal

・SQLiteDatabase.insert
SQLiteDatabaseがSQLiteDatabaseLockedExceptionをキャッチし、呼出し元に-1を返します。
public long insert(String table, String nullColumnHack, ContentValues values) {
    try {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + values, e);
        return -1;
    }
}
・SQLiteDatabase.insertOrThrow
SQLiteDatabaseLockedExceptionが投げられます。
public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
        throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

・SQLiteDatabase.update
SQLiteDatabaseLockedExceptionが投げられます。

・SQLiteDatabase.delete
SQLiteDatabaseLockedExceptionが投げられます。


また、トランザクション開始のbeginTransactionでも同様。

・SQLiteDatabase.beginTransaction
SQLiteDatabaseLockedExceptionが投げられます。


以上です。