Murayama blog.

プログラミングと、その次の話

AndroidでParse入門 - Queries -

元ネタ

こちらを参考にQueriesについてまとめます。
https://parse.com/docs/android_guide#queries

前回の記事はこちら。
AndroidでParse入門 - Objects編 - Murayama blog.

Basic Queries

多くの場合、getInBackgroundでは取得したいオブジェクトを指定するのに向かないでしょう。ParseQueryは、リストオブジェクトや単一オブジェクトを取得するための様々な方法を提供しています。

一般的には、ParseQueryを作成して条件を付与します。そして、findInBackgroundメソッドとFindCallbackを使ってマッチしたParseObjectsのリストを取得します。たとえば、特定のplayerNameのスコアを取得するには、whereEqualToメソッドを使用してキーの値に制約をかけます。

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Dan Stemkoski");
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> scoreList, ParseException e) {
        if (e == null) {
            Log.d("score", "Retrieved " + scoreList.size() + " scores");
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});

findInBackgroundメソッドは、バックグラウンドスレッドでネットワーク要求を確立する点はgetInBackgroundと同じです。コールバックはメインスレッド上で実行します。

Query Constraints

ParseQueryを使ってオブジェクトの検索に制約をかける方法はいくつかあります。whereNotEqualToメソッドを使えば、特定のKey-Valueペアでフィルタリングすることができます。

query.whereNotEqualTo("playerName", "Michael Yabuti");

複数の制約を指定することもできます。AND制約ように、すべての制約を満たしたオブジェクトだけを取得できます。

query.whereNotEqualTo("playerName", "Michael Yabuti");
query.whereGreaterThan("playerAge", 18);

setLimitメソッドを使えば、検索件数に制限をかけることができます。デフォルトの制限数は100ですが、1から1000件まで設定可能です。

query.setLimit(10); // limit to at most 10 results

1件だけ取得したいなら、findメソッドの代わりにgetFirst、getFirstBackgroundメソッドを使うと便利です。

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerEmail", "dstemkoski@example.com");
query.getFirstInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (object == null) {
      Log.d("score", "The getFirst request failed.");
    } else {
      Log.d("score", "Retrieved the object.");
    }
  }
});

setSkipメソッドを使えば先頭の数件をスキップできます。ページネーションに便利でしょう。

query.setSkip(10); // skip the first 10 results

数値型や文字列型はソート可能です。結果の並び順を制御できます。

// Sorts the results in ascending order by the score field
query.orderByAscending("score");
 
// Sorts the results in descending order by the score field
query.orderByDescending("score");

追加のソートキーを指定することもできます。

// Sorts the results in ascending order by the score field if the previous sort keys are equal.
query.addAscendingOrder("score");
 
// Sorts the results in descending order by the score field if the previous sort keys are equal.
query.addDescendingOrder("score");

ソート可能な型は、比較条件としても使えます。

// Restricts to wins < 50
query.whereLessThan("wins", 50);
 
// Restricts to wins <= 50
query.whereLessThanOrEqualTo("wins", 50);
 
// Restricts to wins > 50
query.whereGreaterThan("wins", 50);
 
// Restricts to wins >= 50
query.whereGreaterThanOrEqualTo("wins", 50);

いくつかの異なる値を条件にオブジェクトを取得したいなら、whereContainedInメソッドを使って許可する値のコレクションを指定します。これにより、複数のクエリを1つのクエリに置き換えれることもあるでしょう。たとえば、特定のユーザーリストによって、作成されたスコアを取得したいなら次のように実装します。

String[] names = {"Jonathan Walsh", "Dario Wunsch", "Shawn Simon"};
query.whereContainedIn("playerName", Arrays.asList(names));

逆に、いくつかの異なる値に一致しないオブジェクトを取得したいなら、whereNotContainedInを使って、値のコレクションを指定します。たとえば、リストにあるユーザー以外のスコアを取得したいなら次のように実装します。

String[] names = {"Jonathan Walsh", "Dario Wunsch", "Shawn Simon"};
query.whereNotContainedIn("playerName", Arrays.asList(names));

特定のKeyを保持するオブジェクトを取得したいなら、whereExistsメソッドを使います。逆に、特定のKeyを保持しないオブジェクトを取得したいなら、whereDoesNotExistメソッドを使います。

// Finds objects that have the score set
query.whereExists("score");
 
// Finds objects that don't have the score set
query.whereDoesNotExist("score");

別のクエリの結果セット内のキーの値に一致するオブジェクトを取得したいなら、whereMatchesKeyInQueryメソッドを使用します。たとえば、スポーツチームを含むクラスと、ユーザのhometownを格納しているUserクラスがあるなら、勝率が勝ち越していることを条件にhometownに関連するユーザのリストを1つのクエリで発行することができます。クエリは次のようになります。

ParseQuery<ParseObject> teamQuery = ParseQuery.getQuery("Team");
teamQuery.whereGreaterThan("winPct", 0.5);
ParseQuery<ParseUser> userQuery = ParseUser.getQuery();
userQuery.whereMatchesKeyInQuery("hometown", "city", teamQuery);
userQuery.findInBackground(new FindCallback<ParseUser>() {
  void done(List<ParseUser> results, ParseException e) {
    // results has the list of users with a hometown team with a winning record
  }
});

逆に、別のクエリの結果セット内のキーの値に一致しないオブジェクトを取得したいなら、whereDoesNotMatchKeyInQueryメソッドを使用します。たとえば、チームが負け越していることを条件にhometownに関連するユーザを見つける場合は次のようになります。

ParseQuery<ParseUser> losingUserQuery = ParseUser.getQuery();
losingUserQuery.whereDoesNotMatchKeyInQuery("hometown", "city", teamQuery);
losingUserQuery.findInBackground(new FindCallback<ParseUser>() {
  void done(List<ParseUser> results, ParseException e) {
    // results has the list of users with a hometown team with a losing record
  }
});

キーのコレクションを指定してselectKeysメソッドを呼び出すことで、返却されるフィールドを制限することができます。scoreとplayerNameだけを含む文書を取得するなら、次のようになります(特殊な組み込みフィールドであるobjectId、createdAt、updatedAtは結果に含まれます)。

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.selectKeys(Arrays.asList("playerName", "score"));;
List<ParseObject> results = query.find();

残ったフィールドは、取得済みのオブジェクトに対してfetchIfNeededを呼び出す事でフェッチすることができます。

ParseObject object = results.get(0);
object.fetchIfNeededInBackground(new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    // all fields of the object will now be available here.
  }
});

Queries on Array Values

キーが配列の値を持つ場合、配列の値に2を含むオブジェクトを検索することができます。

// Find objects where the array in arrayKey contains the number 2.
query.whereEqualTo("arrayKey", 2);

配列の値に2、3、4を含むものを検索する場合は次のようになります。

// Find objects where the array in arrayKey contains all of the numbers 2, 3, and 4.
ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(2);
numbers.add(3);
numbers.add(4);
query.whereContainsAll("arrayKey", numbers);

Queries on String Values

whereStartsWithメソッドを使えば、特定の文字列で開始するものだけに制限することができます。MySQLのLIKE演算子とよく似ており、膨大なデータセットを効率良くアクセスできるようindex化されます。

// Finds barbecue sauces that start with "Big Daddy's".
ParseQuery<ParseObject> query = ParseQuery.getQuery("BarbecueSauce");
query.whereStartsWith("name", "Big Daddy's");

Relational Queries

リレーショナルデータのクエリを発行するには、いくつかの方法があります。フィールドが特定のParseObjectにマッチするオブジェクトを取得したいなら、他のデータ型と同様whereEqualToメソッドを使用できます。たとえば、個々のCommentは、postフィールドでPostオブジェクトを参照しているので、特定のPostのCommentは次のようにフェッチできます。

// Assume ParseObject myPost was previously created.
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereEqualTo("post", myPost);
 
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // commentList now has the comments for myPost
  }
});

別のクエリのにマッチするParseObjectをフィールドに持つオブジェクトを取得したいなら、whereMatchesQueryメソッドを使用します。インナークエリもデフォルトのリミットが100件であること、リミットの上限が1000件であることに注意してください。大規模なデータセットの場合、目的を達成するためには、慎重にクエリを組み立てる必要があります。画像を含むPostのCommnetを取得する場合、次のようになります。

ParseQuery<ParseObject> innerQuery = ParseQuery.getQuery("Post");
innerQuery.whereExists("image");
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereMatchesQuery("post", innerQuery);
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // comments now contains the comments for posts with images.
  }
});

逆に、別のクエリのにマッチしないParseObjectをフィールドに持つオブジェクトを取得したいなら、whereDoesNotMatchQueryメソッドを使用します。画像を含まないPostのCommentを取得する場合、次のようになります。

ParseQuery<ParseObject> innerQuery = ParseQuery.getQuery("Post");
innerQuery.whereExists("image");
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereDoesNotMatchQuery("post", innerQuery);
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // comments now contains the comments for posts without images.
  }
});

場合によっては、1つのクエリで関連するオブジェクトの複数の型を取得したいこともあるでしょう。そのような場合はincludeメソッドを使用します。たとえば、最新の10件のCommentと、それに関連するPostを取得したい場合、次のようになります。

ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
 
// Retrieve the most recent ones
query.orderByDescending("createdAt");
 
// Only retrieve the last ten
query.setLimit(10);
 
// Include the post data with each comment
query.include("post");
 
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> commentList, ParseException e) {
    // commentList now contains the last ten comments, and the "post"
    // field has been populated. For example:
    for (ParseObject comment : commentList) {
      // This does not require a network access.
      ParseObject post = comment.getParseObject("post");
      Log.d("post", "retrieved a related post");
    }
  }
});

マルチレベル(多段)でincludeする場合は、ドット表記を使います。CommnetのPostをincludeし、かつ、Postのauthorをincludeする場合は次のようになります。

query.include("post.author");

includeメソッドを複数回呼べば、複数のフィールドをincludeすることもできます。この機能は、getFirst()、getInBackground()のようなParseQueryのヘルパーでも動作します。

Caching Queries

クエリの実行結果をディスク上にキャッシュすると役に立ちます。デバイスがオフラインの状態でもデータを表示したり、起動時のネットワーク接続が完了するまで、以前のデータを表示したりすることができます。Parseはキャッシュ領域が圧迫されると自動的にフラッシュするようになっています。

デフォルトのクエリでは、キャッシュは無効となっています。setCachePolicyメソッドを使うことでキャッシュは有効になります。たとえば、ネットワーク接続を試みて、ネットワークが利用できないならキャッシュデータを利用する場合は次のようになります。

query.setCachePolicy(ParseQuery.CachePolicy.NETWORK_ELSE_CACHE);
query.findInBackground(new FindCallback<ParseObject>() {
public void done(List<ParseObject> scoreList, ParseException e) {
  if (e == null) {
    // Results were successfully found, looking first on the
    // network and then on disk.
  } else {
    // The network was inaccessible and we have no cached data
    // for this query.
  }
});

Parseはいくつかのキャッシュポリシーを提供しています。

  • IGNORE_CACHE

データ読み込み時にキャッシュを利用しません。またキャッシュに保存もしません。IGNORE_CACHEはデフォルトのキャッシュポリシーです。

  • CACHE_ONLY

データ読み込み時にキャッシュのみを使用します。ネットワークは使用しません。キャッシュされた結果が存在しない場合、ParseExceptionが発生します。

  • NETWORK_ONLY

データ読み込み時にキャッシュ利用しませんが、結果をキャッシュに保存します。

  • CACHE_ELSE_NETWORK

データ読み込み時にキャッシュを利用します。キャッシュが利用できない場合はネットワークを利用します。どちらも利用できない場合はParseExceptionが発生します。

  • NETWORK_ELSE_CACHE

データ読み込み時にネットワークを利用します。ネットワークが利用できない場合はキャッシュを利用します。どちらも利用できない場合はParseExceptionが発生します。

  • CACHE_THEN_NETWORK

先にデータ読み込み時にキャッシュを利用します。その後、ネットワークも利用します。つまり、FindCallbackは2回呼ばれることになります。このキャッシュポリシーは、findInBackgroundのような非同期処理で利用すると良いでしょう。

キャッシュの振る舞いを制御したいなら、ParseQueryのメソッドを利用します。以下のような制御が可能です。

  • クエリのキャッシュが存在するか確認したい場合
boolean isInCache = query.hasCachedResult();
  • 特定のクエリのキャッシュを削除したい場合
query.clearCachedResult();
  • すべてのクエリのキャッシュを削除したい場合
ParseQuery.clearAllCachedResults();
  • キャッシュの保持期間を制御したい場合
query.setMaxCacheAge(TimeUnit.DAYS.toMillis(1));
Query caching also works with ParseQuery helpers including getFirst() and getInBackground().

クエリのキャッシュは、ParseQueryのgetFirst() 、getInBackground()ヘルパーでも動作します。

Counting Objects

クエリにマッチするオブジェクトの件数を取得したい(オブジェクト自体は取得したくない)場合は、findメソッドの代わりにcountメソッドを使用します。たとえば、特定のプレイヤーの遊んだゲーム数を取得したいなら次のようになります。

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Sean Plott");
query.countInBackground(new CountCallback() {
  public void done(int count, ParseException e) {
    if (e == null) {
      // The count request succeeded. Log the count
      Log.d("score", "Sean has played " + count + " games");
    } else {
      // The request failed
    }
  }
});

呼び出しもとでスレッドをブロックしたいなら、query.count()とすれば同期をとることができます。

1000件以上のオブジェクトの場合、カウント操作は、タイムアウトによって制限されます。タイムアウトが発生する場合もあれば、おおよそ正しい結果を返すこともあります。そのため、アーキテクトにとっては、この種のカウント操作を避けることが好ましいでしょう。

Compound Queries

複数のクエリのいずれかにマッチするオブジェクトを検索したいなら、arseQuery.orメソッドを使います。たとえば、たくさん勝利しているプレイヤーとほとんど勝利していないプレイヤーを検索したい場合は次のようになります。

ParseQuery<ParseObject> lotsOfWins = ParseQuery.getQuery("Player");
lotsOfWins.whereGreaterThan(150);
 
ParseQuery<ParseObject> fewWins = ParseQuery.getQuery("Player");
fewWins.whereLessThan(5);
 
List<ParseQuery<ParseObject>> queries = new ArrayList<ParseQuery<ParseObject>>();
queries.add(lotsOfWins);
queries.add(fewWins);
 
ParseQuery<ParseObject> mainQuery = ParseQuery.or(queries);
mainQuery.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> results, ParseException e) {
    // results has the list of players that win a lot or haven't won much.
  }
});

合成されたParseQueryに制約を追加することもできます。追加したクエリはAND演算子のように機能します。注意:複合クエリ(compound query)内のサブクエリでは、setLimit、skip、orderByフィルタリング制約はサポートされません。