Como trabalhar com matrizes, listas e conjuntos

Muitos aplicativos precisam armazenar estruturas de dados semelhantes à matriz em documentos. Por exemplo, um documento de usuário pode conter uma lista dos cinco melhores amigos do usuário, ou um documento de postagem do blog pode conter um conjunto de categorias associadas.

Ainda que o Cloud Firestore possa armazenar matrizes, ele não oferece suporte para consulta de membros da matriz ou atualização de elementos únicos da matriz. No entanto, você pode modelar este tipo de dados aproveitando os outros recursos do Cloud Firestore.

Antes de continuar, leia sobre o modelo de dados do Cloud Firestore.

Solução: um mapa de valores

Uma maneira de modelar dados semelhantes à matriz no Cloud Firestore é representá-los como um mapa de chaves para valores booleanos. Por exemplo, suponha que, em seu aplicativo de blogs, cada publicação seja marcada com várias categorias. Se você usa uma matriz, um documento de postagem do blog se parece com o seguinte exemplo:

Web

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

E para consultar todas as postagens que fazem parte da categoria "gatos"? Com a estrutura de dados acima, não é possível executar essa consulta.

Considere esta estrutura de dados alternativa, em que cada categoria é a chave em um mapa e todos os valores são true:

Web

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

Agora é fácil consultar todas as postagens do blog em uma única categoria:

Web

 // 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>() {
            // ...
        });

Esta técnica baseia-se no fato de o Cloud Firestore criar índices internos para todos os campos do documento, mesmo em um mapa aninhado.

Como fazer uma consulta mais complexa, como todas as postagens do blog em uma única categoria ordenadas por data de publicação? Você pode tentar uma consulta como esta:

Consulta inválida

Web

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

Consultas em vários campos com filtros de intervalo exigem um índice composto. Isso não é possível com a abordagem mostrada acima. Os índices precisam ser definidos em caminhos de campo específicos. Você tem que criar um índice em cada caminho de campo possível (como categories.cats ou categories.opinion), o que será impossível de fazer antecipadamente se as categorias forem geradas pelo usuário.

Você pode contornar essa limitação codificando todas as informações da consulta em valores de mapa:

Web

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

Agora você pode expressar a consulta desejada como um conjunto de condições em um único campo, evitando a necessidade de um índice composto:

Consulta válida

Web

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

Comparado ao uso de um mapa de booleanos, essa técnica torna mais difícil a atualização de dados, porque os valores do mapa precisam ser mantidos em sincronia uns com os outros.

Limitações

A solução mostrada acima é uma ótima maneira de simular estruturas semelhantes à matriz no Cloud Firestore. Porém, você deve estar ciente das seguintes limitações:

  • Limites de indexação: um único documento pode ter apenas 20.000 propriedades para usar os índices internos do Cloud Firestore. Se a estrutura de dados semelhante à matriz crescer para dezenas de milhares de membros, você poderá atingir nesse limite.
  • Tamanho do documento: o Cloud Firestore é otimizado para documentos pequenos e restringe o limite de tamanho a 1 MB nos documentos. Se sua matriz puder ser expandida arbitrariamente, será melhor usar uma subcoleção, que apresenta melhor desempenho de escala.