ContentProviderOperationとapplyBatch
- Insert, Update, Delete操作をバッチ処理するメソッド
- バッチ用クエリ ContentProviderOperation の構築
- システムはContentProviderOperationの内容に従ってクエリを発行
- 標準ではAtomic性は保証されていない(が組み込むのは容易)
- バッチオペレーションはfail stop. 失敗したオペレーション以降は実行されない
参考:
- ContentProviderOperationとapplyBatch
- ContentProviderOperation.BuilderのAPI
- ContentProviderOperationで後方参照を活用する
Building ContentProviderOperation.
applyBatchはクエリ群をContentProviderOperationのリストとして受け取る. ContentProviderOperationはinsert/update/deleteクエリを表現するクラスで, これをもとにクエリ生成・発行される.
ContentProviderOperationはContentProviderOperation.Builderクラスで構築する. ContentProviderOperationを構築するサンプルは下記.
// Insertオペレーションを生成
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build()
// Updateオペレーションを生成
ContentProviderOperation.newUpdate(uri).withValue("name", "test2").build()
// Deleteオペレーションを生成
ContentProviderOperation.newDelete(uri).build()
ContentProviderOperation.BuilderはInsert用, Update用, Delete用, 用途にあわせて使用する. Builderは"どの種類のクエリなのか"を明示して生成する.
Builder.newInsert(uri)
: Insert用ContentProviderOperation.Builderを生成
Builder.newUpdate(uri)
: Update用ContentProviderOperation.Builderを生成
Builder.newDelete(uri)
: Delete用ContentProviderOperation.Builderを生成
ContentProviderOperation.Builderには, ContentProviderOperationを構築するためのメソッドが用意されている. 例えばInsert用ビルダでは, Update/Deleteに特化したwithSelection(...)メソッドは使用できない. 各ビルダ種別毎の使用可能/不可能メソッドの一覧はContentProviderOperation.Builderのjavadocで確認できる. 使用不可能なメソッドを呼び出すとIllegalArgumentException例外が投げられる.
ContentProviderOperationでデータの更新内容を表現可能. 指定には下記のメソッドを使用する. これらはDelete用ビルダでは使用できない.
ContentProviderOperation.Builder.withValues(ContentValues)
ContentProviderOperation.Builder.withValueBackReference(String, int)
ContentProviderOperation.Builder.withValueBackReference(ContentValues)
// nameにtest1を持つレコードを追加するInsert文
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build();
ContentProviderOperationで表現されるクエリは条件(selection)の指定が可能. 条件指定には下記のメソッドを使用する. これらはInsert用ビルダでは使用できない.
ContentProviderOperation.Builder.withSelection(String, String[])
ContentProviderOperation.Builder.withSelectionBackReference(int, int)
// _idが3のレコードのnameを"test5"に更新するUpdate文
ContentProviderOperation.newUpdate(uri).withValue("name", "test5")
.withSelection("_id=?", new String[]{"3"}).build();
Back reference
ContentProviderOperationには後方参照機能が備わっている. 後方参照とは"クエリセットの中で前回のクエリ結果を参照する機能". 後方参照を使用して, クエリ結果を別クエリの条件式で使用することができる.
例) 1つめのクエリで追加されたレコードの_id値を, 2つめのクエリで使用する.
- INSERT INTO tbl_A (name, page_number, parent_id) VALUES ('parent', 2, NULL);
- INSERT INTO tbl_B (name, parent_id) VALUES ('child', 777 /* 前クエリで追加したレコードの_id値 */ );
1つめのクエリで発行される_id値を得るはInsert文を発行する必要がある. こういった場合に前回のクエリ結果を後方参照で使用できる.
ContentProviderOperation.Builderには後方参照を設定するためのメソッドが用意されている.
各メソッドの詳細は後述.
- ContentProviderOperation.Builder.withSelectionBackReference(int, int)
- ContentProviderOperation.Builder.withValueBackReference(String, int)
- ContentProviderOperation.Builder.withValueBackReference(ContentValues)
実際にwithSelectionBackReferenceメソッドを使って後方参照してみる.
operations.add(ContentProviderOperation.newInsert(uri)
.withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
.withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)
.withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)
.withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)
.withValue("name", "test5")
.withSelection("_id=?", new String[1])
.withSelectionBackReference(0, 2) // ★
.build());
5つめのオペレーション構築で後方参照を使用し, 抽出条件を"_id=?"としている.
withSelectionBackReferenceの第1引数(selectionArgIndex)
: selectionで'?'パラメータとして渡されたインデックスを指定
withSelectionBackReferenceの第2引数(previousResult)
: これまでに発行されたクエリの中から何番目の結果を使用するかを指定.
例に挙げたコードの場合は1~4つめまでのオペレーションで得られた結果のインデックス(0~3)を指定する(インデックス0ベース).
後方参照でどのような値に置換されるのかは下記に依存する.
- クエリの種類(insert/update/delete)
- ContentProviderのinsert/update/deleteメソッドの戻り値仕様
通常, Insertは追加されたレコードのURIを戻り値とする. Update/Deleteは影響を受けたレコード数を戻り値とする.
以降はそのように実装されている前提で話を進める.
previousResult
後方参照の対象となるクエリがInsertオペレーションである場合
: Insertで追加されたレコードのID値が 後方参照により取得できる.
後方参照の対象となるクエリがUpdate/Deleteオペレーションである場合
: Update/Deleteで変更されたレコードの数が後方参照により取得できる.
内部実装的に, previousResultで指定するインデックスは結果セットそのもの(ContentProviderResult配列)を参照する.
ContentProviderResultはContentProviderのinsert/update/delete結果を格納するが, 後方参照ではInsertの結果をURIではなくint値を返す.
このint値はそのuriに含まれている(であろう)id値, つまり追加されたレコードのid値となる. ただし, レコードのidが取得できるかどうかは保証されておらず, 厳密にはContentProvider.insertの戻り値として返されるURIをContentUris.parseId()で処理した結果にすぎない. 例通り, URIパスの末尾にid値を配置することが推奨される.
実際に後方参照値を取り出すFrameworkソースは下記.
// android.content.ContentProviderOperation.backRefToValue(ContentProviderResult[], int, Integer)
/**
* Return the string representation of the requested back reference.
* @param backRefs an array of results
* @param numBackRefs the number of items in the backRefs array that are valid
* @param backRefIndex which backRef to be used
* @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
* the numBackRefs
* @return the string representation of the requested back reference.
*/
private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
Integer backRefIndex) {
if (backRefIndex >= numBackRefs) {
Log.e(TAG, this.toString());
throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
+ " but there are only " + numBackRefs + " back refs");
}
ContentProviderResult backRef = backRefs[backRefIndex];
long backRefValue;
if (backRef.uri != null) {
backRefValue = ContentUris.parseId(backRef.uri);
} else {
backRefValue = backRef.count;
}
return backRefValue;
}
if (backRef.uri != null) つまりInsertオペレーションによる結果(ContentProviderResult)である場合, uriから_idを抽出する. そうでない(つまりUpdate/Deleteオペレーションによる結果である)場合, countを返すという暗黙的なルールに依存した仕様となっている.
残る下記2つのメソッドについても基本的には同じ考え方. これらはInsert/Updateで追加or更新したいContentValueを指定する後方参照版である.
ContentProviderOperation.Builder.withValueBackReference(String, int)
ContentProviderOperation.Builder.withValueBackReference(ContentValues)
withValueBackReference(String, int)の第2引数が前述のpreviousResultにあたる. 1つ目の引数は更新対象のカラム名.
withValueBackReference(ContentValues)は追加・更新したい値が複数カラムある場合に使う.
ContentValuesのkeyとvalueはwithValueBackReference(String, int)のそれと同じ. ContentValuesのkeyが更新対象のカラム名, valuesが前述のpreviousResultにあたる.
下記はそのサンプル.
// nameの値がpreviousResult 2番目の結果で更新される
ContentValues v = new ContentValues();
v.put("name", 2);
operations.add(ContentProviderOperation.newInsert(uri)
.withValue("name", "electric sheep")
.withValueBackReferences(v).build());
Execute applyBatch!
ContentProviderOperationを構築すればあとはapplyBatchを実行するのみ.
ArrayList<ContentProviderOperation> operations
= new ArrayList<ContentProviderOperation>();
Uri uri = Uri.parse("content://yuki.mycp");
operations.add(ContentProviderOperation.newInsert(uri)
.withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
.withValue("name", "test2").build());
try {
getContentResolver().applyBatch("yuki.mycp", operations);
} catch (Exception e) {
Log.e("yuki", "error");
}
これでInsert文とUpdate文がまとめて実行される. クエリはArrayListの先頭から順番に実行されていく. もし途中でクエリの実行が失敗すると, それ以降のクエリは実行されない.
ContentProvider側に手を入れる必要はなく, applyBatchは実行できる. ただし標準のapplyBatchはAtomic性を保証していない.
Atomic operation.
applyBatchのAtomic性を保証したい場合, 独自のプロバイダでapplyBatchをオーバーライドする.
@Override
public ContentProviderResult[] applyBatch(
ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
SQLiteDatabase db = MyDataBase.getInstance(getContext())
.getWritableDatabase();
try {
db.beginTransaction();
ContentProviderResult[] result = super.applyBatch(operations);
db.setTransactionSuccessful();
return result;
} finally {
db.endTransaction();
}
}
これでapplyBatchのAtomic性が保証される.
以上.