配列、リスト、セットの操作

多くのアプリでは、配列のようなデータ構造をドキュメントに保存する必要があります。たとえば、ユーザー ドキュメントには、ユーザーの上位 5 人の友人のリストが含まれていたり、ブログ投稿ドキュメントには関連カテゴリのセットが含まれている場合があります。

Cloud Firestore では配列を保存できますが、配列メンバーのクエリや単一配列要素の更新はサポートしていません。しかし、Cloud Firestore の他の機能を活用することで、この種のデータをモデル化することができます。

続行する前に、Cloud Firestore のデータモデルについてお読みください。

解決策: 値のマップ

Cloud Firestore で配列のようなデータをモデル化する 1 つの方法は、キーからブール値へのマップとして表すことです。たとえば、ブログのアプリで各投稿に多数のカテゴリがタグ付けされているとします。配列を使用する場合、ブログ投稿のドキュメントは次の例のようになります。

ウェブ

// Sample document in the 'posts' collection.
{
    title: "My great post",
    categories: [
        "technology",
        "opinion",
        "cats"
    ]
}

Swift

struct PostArray {

    let title: String
    let categories: [String]

    init(title: String, categories: [String]) {
        self.title = title
        self.categories = categories
    }

}

let myArrayPost = PostArray(title: "My great post",
                            categories: ["technology", "opinion", "cats"])

Android

public class ArrayPost {
    String title;
    String[] categories;

    public ArrayPost(String title, String[] categories) {
        this.title = title;
        this.categories = categories;
    }
}

ArrayPost myArrayPost = new ArrayPost("My great post", new String[]{
        "technology", "opinion", "cats"
});

「cats」カテゴリの一部であるすべての投稿に対してクエリを実行するにはどうすればよいでしょうか。上記のデータ構造では、このクエリを実行する方法はありません。

この代替データ構造を考えてみましょう。各カテゴリはマップ内のキーであり、すべての値が true です。

ウェブ

// Sample document in the 'posts' collection
{
    title: "My great post",
    categories: {
        "technology": true,
        "opinion": true,
        "cats": true
    }
}

Swift

struct PostDict {

    let title: String
    let categories: [String: Bool]

    init(title: String, categories: [String: Bool]) {
        self.title = title
        self.categories = categories
    }

}

let post = PostDict(title: "My great post", categories: [
    "technology": true,
    "opinion": true,
    "cats": true
])

Android

public class MapPost {
    String title;
    Map<String,Boolean> categories;

    public MapPost(String title, Map<String,Boolean> categories) {
        this.title = title;
        this.categories = categories;
    }
}

Map<String, Boolean> categories = new HashMap<>();
categories.put("technology", true);
categories.put("opinion", true);
categories.put("cats", true);
MapPost myMapPost = new MapPost("My great post", categories);

これで、1 つのカテゴリ内のすべてのブログ投稿に対して簡単にクエリを実行できます。

ウェブ

 // Find all documents in the 'posts' collection that are
// in the 'cats' category.
db.collection('posts')
    .where('categories.cats', '==', true)
    .get()
    .then(() => {
        // ...
    });)

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .getDocuments() { (querySnapshot, err) in

        // ...

}

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .get()
        .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            // ...
        });

この手法が可能になるのは、Cloud Firestore の特長として、ネストされたマップ内のフィールドであっても、すべてのドキュメント フィールドに対して組み込みインデックスが作成されるからです。

単一カテゴリのすべてのブログ投稿を投稿日順に並べるなど、より複雑なクエリを実行するにはどうすればよいでしょうか。次のようなクエリを試してみてください。

無効なクエリ

ウェブ

 db.collection('posts')
    .where('categories.cats', '==', true)
    .orderBy('timestamp');)

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .order(by: "timestamp")

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .orderBy("timestamp");

範囲フィルタを使用して複数のフィールドにクエリを実行するには、複合インデックスが必要です。しかし、上記の手法ではこれが不可能です。インデックスは、特定のフィールドパスに定義する必要があります。したがって、categories.catscategories.opinion などの可能なフィールドパスごとにインデックスを作成しなければなりませんが、カテゴリがユーザー生成されている場合にはこれを事前に行うことができません。

この制限を回避するには、クエリのすべての情報をマップ値にエンコードします。

ウェブ

// The value of each entry in 'categories' is a unix timestamp
{
  title: "My great post",
  categories: {
    technology: 1502144665,
    opinion: 1502144665,
    cats: 1502144665
  }
}

Swift

struct PostDictAdvanced {

    let title: String
    let categories: [String: UInt64]

    init(title: String, categories: [String: UInt64]) {
        self.title = title
        self.categories = categories
    }

}

let dictPost = PostDictAdvanced(title: "My great post", categories: [
    "technology": 1502144665,
    "opinion": 1502144665,
    "cats": 1502144665
])

Android

public class MapPostAdvanced {
    String title;
    Map<String,Long> categories;

    public MapPostAdvanced(String title, Map<String,Long> categories) {
        this.title = title;
        this.categories = categories;
    }
}

Map<String, Long> categories = new HashMap<>();
categories.put("technology", 1502144665L);
categories.put("opinion", 1502144665L);
categories.put("cats", 1502144665L);
MapPostAdvanced myMapPostAdvanced = new MapPostAdvanced("My great post", categories);

これで、複合インデックスの必要性なく、1 つのフィールドに対する一連の条件として、目的のクエリを表現できるようになりました。

有効なクエリ

ウェブ

 db.collection('posts')
    .where('categories.cats', '>', 0)
    .orderBy('categories.cats');)

Swift

db.collection("posts")
    .whereField("categories.cats", isGreaterThan: 0)
    .order(by: "categories.cats")

Android

db.collection("posts")
        .whereGreaterThan("categories.cats", 0)
        .orderBy("categories.cats");

ブール値のマップを使用する場合と比較して、この手法ではマップ値を互いに同期させておく必要があるため、データの更新が難しくなります。

制限事項

上記の解決策は、Cloud Firestore の配列のような構造をシミュレートする優れた方法ですが、次の制限事項に注意する必要があります。

  • インデックス作成の制限 - Cloud Firestore の組み込みインデックスを使用するために、1 つのドキュメントで使用できるプロパティの数は 20,000 個のみです。配列のようなデータ構造が数万のメンバーになると、この制限に達することがあります。
  • ドキュメント サイズ - Cloud Firestore は小さなドキュメントに最適化されており、ドキュメントに 1 MB のサイズ制限を適用します。配列を任意に展開できる場合は、スケーリングのパフォーマンスに優れたサブコレクションを使用することをおすすめします。

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。