Lea y escriba datos

Crea prototipos y realiza pruebas con Firebase Emulator Suite (opcional)

Antes de analizar cómo tu app realiza operaciones de lectura y escritura en Realtime Database, conoceremos Firebase Emulator Suite, un conjunto de herramientas con el que puedes crear prototipos y probar las funciones de Realtime Database. Si quieres probar diferentes modelos de datos, optimizar tus reglas de seguridad o encontrar la forma más rentable de interactuar con el backend, recomendamos que trabajes a nivel local sin implementar servicios en ejecución.

Los emuladores de Realtime Database forman parte de Local Emulator Suite, lo que permite que tu app interactúe con el contenido y la configuración emulados de la base de datos y, si lo deseas, con los recursos emulados del proyecto (funciones, otras bases de datos y reglas de seguridad).

Para usar el emulador de Realtime Database, solo debes seguir estos pasos:

  1. Agrega una línea de código a la configuración de prueba de tu app para conectarte al emulador.
  2. Desde la raíz del directorio de tu proyecto local, ejecuta firebase emulators:start.
  3. Realiza llamadas desde el código prototipo de tu app con un SDK de la plataforma de Realtime Database como de costumbre, o bien usa la API de REST de Realtime Database.

Hay una explicación detallada sobre Realtime Database y Cloud Functions disponible. También deberías consultar la Introducción a Emulator Suite.

Obtén una DatabaseReference

Para leer o escribir en la base de datos, necesitas una instancia de DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref();

Escribe datos

Este documento abarca los conceptos básicos de la lectura y la escritura de datos en Firebase.

Los datos de Firebase se escriben en un DatabaseReference y se recuperan cuando se esperan o detectan eventos emitidos por la referencia. Los eventos se emiten una vez para el estado inicial de los datos y otra vez cuando cambian los datos.

Operaciones básicas de escritura

Si quieres ejecutar operaciones básicas de escritura, puedes usar set() para guardar datos en una referencia específica y reemplazar todos los datos ubicados en esa ruta de acceso. Puedes establecer una referencia a los siguientes tipos: String, boolean, int, double, Map y List.

Por ejemplo, puedes agregar un usuario con set() como se muestra a continuación:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Si usas set() de esta forma, se reemplazan los datos en la ubicación especificada, incluidos los nodos secundarios. Sin embargo, es posible actualizar un elemento secundario sin volver a escribir el objeto entero. Si deseas permitir que los usuarios actualicen sus perfiles, podrías actualizar el nombre de usuario de la siguiente forma:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

El método update() acepta una ruta secundaria a los nodos, lo que te permite actualizar varios nodos en la base de datos a la vez:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Lee datos

Lee datos detectando eventos de valor

Si quieres leer datos de una ruta de acceso y detectar los posibles cambios, usa la propiedad onValue de DatabaseReference para detectar DatabaseEvents.

Puedes usar el DatabaseEvent para leer los datos de una ruta de acceso determinada en el estado en que se encontraban en el momento del evento. Este evento se activa cuando se adjunta el objeto de escucha y se vuelve a activar cada vez que cambian los datos (incluidos los de segundo nivel). El evento tiene una propiedad snapshot que contiene todos los datos de dicha ubicación, incluidos los datos secundarios. Si no hay datos, la propiedad exists de la instantánea será false y su propiedad value será nula.

En el siguiente ejemplo, se demuestra una aplicación de blogs sociales que recupera los detalles de una publicación de la base de datos:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

El objeto de escucha recibe una DataSnapshot que contiene los datos en la ubicación especificada en el momento del evento en su propiedad value.

Lee los datos una sola vez

Realiza operaciones de lectura una vez con get()

El SDK se diseñó para administrar interacciones con servidores de bases de datos, sin importar si tu app está en línea o sin conexión.

En general, debes usar las técnicas de eventos de valor que se describieron antes a fin de leer datos para recibir notificaciones sobre las actualizaciones de los datos del backend. Estas técnicas reducen el uso y la facturación, y están optimizadas para brindar a los usuarios la mejor experiencia en línea y sin conexión.

Si necesitas los datos solo una vez, puedes usar get() para obtener una instantánea de la base de datos. Si, por algún motivo, get() no puede mostrar el valor del servidor, el cliente sondeará la caché de almacenamiento local y mostrará un error si de todos modos no encuentra el valor.

En el siguiente ejemplo, se muestra cómo recuperar un nombre de usuario público una sola vez desde la base de datos:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

El uso innecesario de get() puede aumentar la utilización del ancho de banda y reducir el rendimiento. Esto se puede evitar mediante un objeto de escucha en tiempo real, como se muestra arriba.

Lee datos una sola vez con once()

En algunos casos, es recomendable que se muestre de inmediato el valor de la caché local, en lugar de buscar un valor actualizado en el servidor. En esos casos, puedes usar once() para obtener los datos de la memoria caché de disco local inmediatamente.

Esto resulta útil para los datos que solo se deben cargar una vez y que no se espera que cambien con frecuencia ni necesiten una escucha activa. Por ejemplo, en la app de blogs de los ejemplos anteriores se usa este método para cargar el perfil de un usuario cuando este comienza a crear una publicación nueva:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Actualiza o borra datos

Actualiza campos específicos

Para escribir de forma simultánea en elementos secundarios específicos de un nodo sin sobrescribir otros nodos secundarios, usa el método update().

Cuando llamas a update(), puedes especificar una ruta de acceso de la clave para actualizar valores secundarios de nivel inferior. Si se almacenan datos en varias ubicaciones para obtener un mejor escalamiento, puedes actualizar todas las instancias de esos datos mediante fan-out de datos. Por ejemplo, es posible que una app social de blogs desee crear una publicación y que esta aparezca en forma simultánea con el feed de actividad reciente y en el feed de actividad de las entradas del usuario. Para ello, la aplicación de blogs usa código similar al siguiente:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

En este ejemplo, se usa push() para crear una entrada en el nodo que contiene las entradas de todos los usuarios en /posts/$postid y recuperar la clave con key de manera simultánea. Luego, se puede usar la clave para crear una segunda entrada en las publicaciones del usuario en /user-posts/$userid/$postid.

Con estas rutas de acceso, puedes ejecutar actualizaciones simultáneas en varias ubicaciones del árbol JSON con una única llamada a update(), de manera similar a este ejemplo en el que se crea la publicación nueva en ambas ubicaciones. Las actualizaciones simultáneas que se hacen de esta forma son atómicas: todas se ejecutan correctamente o todas fallan.

Agrega una devolución de llamada de finalización

Si quieres saber en qué momento se confirma la escritura de los datos, puedes registrar devoluciones de llamada de finalización. Tanto set() como update() muestran Future, a las que puedes adjuntar devoluciones de llamada de éxito y error a las que se llama cuando la escritura se confirma en la base de datos y cuando la llamada no funciona.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Borra datos

La forma más sencilla de borrar datos es llamar a remove() en una referencia a la ubicación de los datos.

También puedes borrar datos si especificas como nulo el valor de otra operación de escritura, como set() o update(). Puedes usar esta técnica con update() para borrar varios objetos secundarios con una sola llamada a la API.

Guarda datos como transacciones

Cuando trabajas con datos que se podrían dañar si se hacen cambios simultáneos (por ejemplo, contadores incrementales) puedes pasar un controlador de transacciones a runTransaction() para usar una transacción. Los controladores de transacciones toman el estado actual de los datos como argumento y muestran el nuevo estado que deseas escribir. Si otro cliente escribe en la ubicación antes de que tu valor nuevo se escriba correctamente, se hace una nueva llamada a la función de actualización con el nuevo valor actual y se vuelve a intentar la operación de escritura.

Por ejemplo, en la app de blogs sociales de ejemplo, podrías permitir que los usuarios agreguen o quiten estrellas en las entradas y llevar un seguimiento de cuántas estrellas recibió una entrada de la siguiente forma:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

De forma predeterminada, los eventos se generan cada vez que se ejecuta la función de actualización de transacciones. Por lo tanto, si ejecutas la función varias veces, es posible que veas estados intermedios. Puedes configurar applyLocally como false para suprimir estos estados intermedios y esperar hasta que se complete la transacción antes de que se generen los eventos:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

El resultado de una transacción es un TransactionResult, que contiene información como la confirmación de la transacción y la nueva instantánea:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Cancela una transacción

Si quieres cancelar una transacción de forma segura, llama a Transaction.abort() para arrojar una AbortTransactionException:

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

Incrementos atómicos del servidor

En el caso de uso anterior, se escriben dos valores en la base de datos: el ID del usuario que destacó o dejó de destacar la entrada, y el recuento de estrellas general. Si ya sabemos que el usuario destacará la entrada, podemos usar una operación de incremento atómico en vez de una transacción.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

Este código no usa una operación de transacción, por lo que no se vuelve a ejecutar automáticamente si hay una actualización conflictiva. Sin embargo, como la operación de incremento ocurre directamente en el servidor de la base de datos, no hay posibilidades de que ocurran conflictos.

Si quieres detectar y rechazar conflictos específicos de la aplicación (por ejemplo, si un usuario destaca una entrada que ya destacó antes), debes escribir reglas de seguridad personalizadas para ese caso de uso.

Trabaja con datos sin conexión

Si un cliente pierde la conexión de red, la app continúa funcionando de manera correcta.

Todos los clientes conectados a una base de datos de Firebase mantienen su propia versión interna de los datos activos. Cuando se escriben datos, se hace primero en esta versión local. Después, el cliente de Firebase sincroniza esos datos con los servidores de bases de datos remotas y con otros clientes según el “mejor esfuerzo”.

Como resultado, todas las operaciones de escritura en la base de datos activan eventos locales al instante, antes de que se escriban datos en el servidor. Esto significa que la app conserva la capacidad de respuesta, sin importar la latencia o el estado de conexión de la red.

Cuando se restablece la conectividad, la app recibe el conjunto de eventos adecuado, de manera que el cliente se sincroniza con el estado actual del servidor sin tener que escribir código personalizado.

Obtén más detalles sobre el comportamiento sin conexión en Más información sobre las capacidades en línea y sin conexión.

Próximos pasos