使用数组、列表和集

许多应用需要在文档中存储类似于数组的数据结构。例如,用户文档可能包含此用户联系最多的五位朋友的列表,或者博文文档可能包含一组关联的类别。

尽管 Cloud Firestore 可以存储数组,但它不支持查询数组成员或更新单个数组元素。但是,您仍然可以利用 Cloud Firestore 的其他功能来对此类数据建模。

继续之前,请确保您已阅读有关 Cloud Firestore 数据模型的内容。

解决方案:值映射

在 Cloud Firestore 中对类数组数据建模的一种方法是将其表示为键到布尔值的映射。例如,假设在博客应用中,每篇博文都标记有多个类别。如果您使用数组,则博文文档如下所示:

网页

// 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"
});

如果您想要查询属于“猫”类别的所有博文,该怎么办?使用上述数据结构将无法执行此查询。

请考虑以下替代数据结构,其中每个类别都是映射关系中的键,且所有值都为 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);

现在您即可轻松查询单个类别中的所有博文:

网页

 // 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);

现在,您可以将所需的查询表达为单个字段上的一组条件,以免需要使用复合索引:

有效查询

网页

 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 内置索引,单个文档只能有 2 万个属性。如果您的类数组数据结构增长到有数万个成员,则可能会受此限制约束。
  • 文档大小 - Cloud Firestore 是为小型文档优化的,并对文档强制执行 1MB 的大小限制。如果您的数组可能会无限扩展,最好使用一个子集合,因为子集合在扩展方面表现得更好。

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面