2012/08/02

Android:ContentProviderOperationで後方参照を活用する


ContentProviderOperationには後方参照の機能が備わっています。
ここでの後方参照とは"クエリセット中における、前回の結果を参照する機能"です。

後方参照を使用すると、クエリ結果を別クエリの条件式で使用することができるようになります。
例えば下記のようなケースです。
  • クエリ1:page_numberが2のレコードを追加
  • クエリ2:クエリ1のレコードを親に持つ子レコードを追加
SQLに置き換えてみると下記のような感じでしょうか...
  • クエリ1:INSERT INTO tbl_A (name, page_number, parent_id) VALUES ('parent', 2, NULL);
  • クエリ2:INSERT INTO tbl_B (name, parent_id) VALUES ('child', 777 /* クエリ1で追加したレコードの_id値 */ );
クエリ2がクエリ1で追加したレコードの_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つめのオペレーション構築で後方参照指定していますね。
selection、つまり抽出条件を"_id=?"としています。

withSelectionBackReferenceの第一引数はselectionArgIndex。
selectionで'?'パラメータとして渡されたインデックスを指定し、これを置換します。
何に置換されるかは第二引数のpreviousResultで決定されます。

previousResultには「何番目の結果を参照するか?」を指定します。
この場合は1~4つめまでのオペレーションで得られた結果のインデックス(0~3)を指定します。
previousResultに0を指定すれば1つめのオペレーションの結果が、1を指定すれば2つめのオペレーションの結果が得られます。
previousResultが指定する参照先は、applyBatchの戻り値でもあるContentProviderResultの配列、つまり結果セットそのものです。

参照された結果セット(ContentProviderResult配列)はどのような値を返すのか?後方参照でどのような値に置換されるのか?
これは、参照するContentProviderResultの種類(Insert or Update or Delete)によって変化します。
また、プロバイダのinsert/updateやdeleteがどのような戻り値を返すのかにも依存します。
通常、Insertは追加されたレコードのURI、Update/Deleteは影響を受けたレコード数が戻り値となりますね。
以降は、そのように実装されている前提で話を進めます。

後方参照の対象となる結果がInsertオペレーションによるものの場合。
後方参照の結果、Insertで追加されたレコードのID値が取得できます。

後方参照の対象となる結果がUpdate/Deleteオペレーションによるものの場合。
後方参照の結果、Update/Deleteで変更されたレコードの数が取得できます。

ContentProviderResultはInsertオペレーションによる結果はuriを保持し、Update/Deleteオペレーションによる結果はcountを保持すると説明しました。
しかし、後方参照ではInsertの結果をuriではなく、そのuriに含まれている(であろう)id値、つまり追加されたレコードの_id値が取得できます。
これは、後方参照用にContentProviderResultのuriを解析して、末尾の数字(つまり_id)を抽出しているためです。

実際に後方参照値を取り出すソースコードを見てみましょう。

・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を返す仕様です。

ここで、もう一度オペレーション構築のコードに戻ります。
今度はコメントをつけてみました。
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 0番目
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 1番目
        .withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 2番目
        .withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 3番目
        .withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 4番目
        .withValue("name", "test5")
        .withSelection("_id=?", new String[1])          // selectionArgIndex=0は_idに対する?
        .withSelectionBackReference(0, 2)               // selectionArgIndex=0, previousResult=2
5番目のオペレーションは後方参照の結果、下記のようなUpdate文になるでしょう。
WHEREで指定される_id値は、3つめのオペレーションで追加されたレコードIDの結果次第で変化します。
UPDATE test_table SET name = 'test5' WHERE _id = 777 /* 3つめのオペレーションで追加されたレコードID */ ;

以上が後方参照の基本的な動作です。

残る下記2つのメソッドについても基本的には同じ考え方です。
これらは、Insert/Updateで追加or更新したい値を指定するwithValue()の後方参照版です。
  • 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", "test995")
        .withValueBackReferences(v).build());
後方参照は少しややこしいですが、
  • previousResultは結果セットの何番目を参照するかを指定する
  • 後方参照で得られる値はInsertの場合uri末尾の数字(_id値)、Update/Deleteは影響を受けたレコード数になる
ことを覚えておけば理解しやすいと思います。

以上です。