Ir a la consola

Trabajar con arreglos, listas y conjuntos

Muchas apps necesitan almacenar estructuras de datos similares a arreglos en documentos. Por ejemplo, un documento de un usuario podría contener una lista de los cinco amigos más íntimos del usuario o un documento de entrada de blog podría contener un conjunto de categorías asociadas.

Pese a que Cloud Firestore puede almacenar arreglos, no admite consultas a miembros de arreglos ni actualizaciones en elementos individuales de un arreglo. Sin embargo, de todas formas, puedes aprovechar otras funciones de Cloud Firestore para modelar este tipo de datos.

Antes de continuar, asegúrate de leer sobre el modelo de datos de Cloud Firestore.

Solución: Un mapa de valores

Una manera de modelar datos similares a un arreglo en Cloud Firestore es representarlos como un mapa que vincula claves y valores booleanos. Por ejemplo, supongamos que creaste una app de blogs en la que cada publicación tiene etiquetas correspondientes a varias categorías. Si usas un arreglo, un documento de una entrada de blog luce como el siguiente ejemplo:

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

¿Qué ocurre si quieres consultar todas las publicaciones que pertenecen a la categoría "cats"? Con la estructura de datos anterior, no hay forma de realizar esta consulta.

Considera esta estructura de datos alternativa, en la que cada categoría es la clave en un mapa y todos los valores son 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);

Ahora es fácil consultar todas las entradas de blog incluidas en una categoría:

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 depende del hecho de que Cloud Firestore crea índices integrados para todos los campos de documentos, incluso los campos en un mapa anidado.

¿Qué ocurre si deseas hacer una consulta más compleja, como todas las entradas de blog en una sola categoría ordenadas según su fecha de publicación? Podrías intentar una consulta como la siguiente:

Consulta no vá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");

Las consultas sobre varios campos con filtros de rango necesitan un índice compuesto. Lamentablemente, esto no es posible con el enfoque anterior. Los índices se deben definir en rutas de campo específicas. Tendrías que crear un índice en cada ruta de campo posible (como categories.cats o categories.opinion), lo cual es imposible de hacer por adelantado si tus categorías son generadas por el usuario.

Para evitar esta limitación, puedes codificar toda la información para la consulta en los valores del 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);

Ahora, puedes expresar la consulta deseada como un conjunto de condiciones en un solo campo y evitar la necesidad de un índice compuesto.

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

En comparación con el uso de un mapa de booleanos, esta técnica dificulta la actualización de los datos, ya que los valores del mapa se deben mantener sincronizados entre sí.

Limitaciones

La solución que vimos resulta muy adecuada para simular estructuras similares a arreglos en Cloud Firestore, pero debes tener en cuenta las siguientes limitaciones:

  • Límites de indexación: Un mismo documento puede tener solo 20,000 propiedades para usar los índices integrados de Cloud Firestore. Si la estructura de datos similar a un arreglo aumenta hasta decenas de miles de miembros, podrías llegar a este límite.
  • Tamaño del documento: Cloud Firestore está optimizado para documentos pequeños y les aplica un límite de tamaño de 1 MB. Si tu matriz puede expandirse de manera arbitraria, es mejor usar una subcolección, que tiene un mejor escalamiento.