Leer y escribir datos

(Opcional) Prototipo y prueba con Firebase Emulator Suite

Antes de hablar sobre cómo su aplicación lee y escribe en Realtime Database, presentemos un conjunto de herramientas que puede usar para crear prototipos y probar la funcionalidad de Realtime Database: Firebase Emulator Suite. Si está probando diferentes modelos de datos, optimizando sus reglas de seguridad o trabajando para encontrar la forma más rentable de interactuar con el back-end, poder trabajar localmente sin implementar servicios en vivo puede ser una gran idea.

Un emulador de base de datos en tiempo real es parte de Emulator Suite, que permite que su aplicación interactúe con el contenido y la configuración de su base de datos emulada, así como, opcionalmente, con los recursos de su proyecto emulado (funciones, otras bases de datos y reglas de seguridad).emulator_suite_short

Usar el emulador de Realtime Database implica solo unos pocos pasos:

  1. Agregar una línea de código a la configuración de prueba de su aplicación para conectarse al emulador.
  2. Desde la raíz del directorio de su proyecto local, ejecute firebase emulators:start .
  3. Realizar llamadas desde el código prototipo de su aplicación usando un SDK de plataforma Realtime Database como de costumbre, o usando la API REST de Realtime Database.

Está disponible un tutorial detallado sobre la base de datos en tiempo real y las funciones de la nube . También deberías echar un vistazo a la introducción a Emulator Suite .

Obtener una referencia de base de datos

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

DatabaseReference ref = FirebaseDatabase.instance.ref();

Escribir datos

Este documento cubre los conceptos básicos de lectura y escritura de datos de Firebase.

Los datos de Firebase se escriben en una DatabaseReference y se recuperan esperando o escuchando los eventos emitidos por la referencia. Los eventos se emiten una vez para el estado inicial de los datos y nuevamente cada vez que cambian los datos.

Operaciones básicas de escritura

Para operaciones básicas de escritura, puede usar set() para guardar datos en una referencia específica, reemplazando cualquier dato existente en esa ruta. Puede establecer una referencia a los siguientes tipos: String , boolean , int , double , Map , List .

Por ejemplo, puedes agregar un usuario con set() de la siguiente manera:

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

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

El uso set() de esta manera sobrescribe los datos en la ubicación especificada, incluidos los nodos secundarios. Sin embargo, aún puedes actualizar un elemento secundario sin tener que reescribir todo el objeto. Si desea permitir que los usuarios actualicen sus perfiles, puede actualizar el nombre de usuario de la siguiente manera:

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 subruta a los nodos, lo que le 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",
});

leer datos

Leer datos escuchando eventos de valor

Para leer datos en una ruta y escuchar cambios, use la propiedad onValue de DatabaseReference para escuchar DatabaseEvent s.

Puede utilizar DatabaseEvent para leer los datos en una ruta determinada, tal como existen en el momento del evento. Este evento se activa una vez cuando se adjunta el oyente y nuevamente cada vez que cambian los datos, incluidos los secundarios. El evento tiene una propiedad snapshot que contiene todos los datos en esa 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.

El siguiente ejemplo 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 oyente recibe una DataSnapshot que contiene los datos en la ubicación especificada en la base de datos en el momento del evento en su propiedad value .

Leer datos una vez

Leer una vez usando get()

El SDK está diseñado para gestionar interacciones con servidores de bases de datos, ya sea que su aplicación esté en línea o fuera de línea.

Generalmente, debe utilizar las técnicas de eventos de valor descritas anteriormente para leer datos y recibir notificaciones sobre las actualizaciones de los datos desde el backend. Esas técnicas reducen su uso y facturación, y están optimizadas para brindarles a sus usuarios la mejor experiencia cuando se conectan y desconectan.

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

El siguiente ejemplo demuestra cómo recuperar el nombre de usuario público de un usuario 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 el uso del ancho de banda y provocar una pérdida de rendimiento, lo que se puede evitar utilizando un escucha en tiempo real como se muestra arriba.

Leer datos una vez con once()

En algunos casos, es posible que desee que el valor de la caché local se devuelva inmediatamente, en lugar de buscar un valor actualizado en el servidor. En esos casos, puede utilizar once() para obtener los datos del caché del disco local inmediatamente.

Esto es útil para datos que solo deben cargarse una vez y no se espera que cambien con frecuencia ni requieran una escucha activa. Por ejemplo, la aplicación de blogs de los ejemplos anteriores utiliza este método para cargar el perfil de un usuario cuando comienza a escribir una nueva publicación:

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

Actualizar o eliminar datos

Actualizar campos específicos

Para escribir simultáneamente en hijos específicos de un nodo sin sobrescribir otros nodos hijos, utilice el método update() .

Al llamar update() , puede actualizar los valores secundarios de nivel inferior especificando una ruta para la clave. Si los datos se almacenan en varias ubicaciones para escalar mejor, puede actualizar todas las instancias de esos datos mediante la distribución en abanico de datos . Por ejemplo, una aplicación de blogs sociales podría querer crear una publicación y actualizarla simultáneamente a la fuente de actividad reciente y a la fuente de actividad del usuario que publica. Para hacer esto, la aplicación de blogs usa un código como este:

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

Este ejemplo usa push() para crear una publicación en el nodo que contiene publicaciones para todos los usuarios en /posts/$postid y simultáneamente recupera la clave con key . Luego, la clave se puede usar para crear una segunda entrada en las publicaciones del usuario en /user-posts/$userid/$postid .

Con estas rutas, puede realizar actualizaciones simultáneas en varias ubicaciones en el árbol JSON con una sola llamada a update() , como en este ejemplo se crea la nueva publicación en ambas ubicaciones. Las actualizaciones simultáneas realizadas de esta manera son atómicas: o todas las actualizaciones tienen éxito o todas fallan.

Agregar una devolución de llamada de finalización

Si desea saber cuándo se han confirmado sus datos, puede registrar devoluciones de llamada de finalización. Tanto set() como update() devuelven Future s, a los que puede adjuntar devoluciones de llamada de éxito y error que se llaman cuando la escritura se ha confirmado en la base de datos y cuando la llamada no tuvo éxito.

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

Borrar datos

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

También puede eliminar especificando null como valor para otra operación de escritura como set() o update() . Puede utilizar esta técnica con update() para eliminar varios elementos secundarios en una sola llamada API.

Guardar datos como transacciones

Cuando trabaje con datos que podrían resultar dañados por modificaciones simultáneas, como contadores incrementales, puede usar una transacción pasando un controlador de transacciones a runTransaction() . Un controlador de transacciones toma el estado actual de los datos como argumento y devuelve el nuevo estado deseado que le gustaría escribir. Si otro cliente escribe en la ubicación antes de que su nuevo valor se escriba correctamente, se llama nuevamente a su función de actualización con el nuevo valor actual y se vuelve a intentar la escritura.

Por ejemplo, en la aplicación de blogs sociales de ejemplo, podría permitir a los usuarios destacar y quitar publicaciones y realizar un seguimiento de cuántas estrellas ha recibido una publicación de la siguiente manera:

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 que si ejecuta la función varias veces, es posible que vea estados intermedios. Puede configurar applyLocally en false para suprimir estos estados intermedios y, en su lugar, esperar hasta que la transacción se haya completado antes de que se generen eventos:

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

El resultado de una transacción es TransactionResult , que contiene información como si la transacción se confirmó 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

Cancelar una transacción

Si desea cancelar una transacción de forma segura, llame a Transaction.abort() para generar una AbortTransactionException :

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

  // ...
});

print(result.committed); // false

Incrementos atómicos del lado del servidor

En el caso de uso anterior, escribimos dos valores en la base de datos: el ID del usuario que destaca o quita la estrella de la publicación y el recuento de estrellas incrementado. Si ya sabemos que el usuario protagoniza la publicación, podemos usar una operación de incremento atómico en lugar 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 utiliza una operación de transacción, por lo que no se vuelve a ejecutar automáticamente si hay una actualización conflictiva. Sin embargo, dado que la operación de incremento ocurre directamente en el servidor de la base de datos, no hay posibilidad de que se produzca un conflicto.

Si desea detectar y rechazar conflictos específicos de una aplicación, como un usuario que destaca una publicación que ya destacó anteriormente, debe escribir reglas de seguridad personalizadas para ese caso de uso.

Trabajar con datos sin conexión

Si un cliente pierde su conexión de red, su aplicación seguirá funcionando correctamente.

Cada cliente conectado a una base de datos de Firebase mantiene su propia versión interna de cualquier dato activo. Cuando se escriben datos, primero se escriben en esta versión local. Luego, el cliente de Firebase sincroniza esos datos con los servidores de bases de datos remotos y con otros clientes al "mejor esfuerzo".

Como resultado, todas las escrituras en la base de datos desencadenan eventos locales inmediatamente, antes de que se escriba cualquier dato en el servidor. Esto significa que su aplicación sigue respondiendo independientemente de la latencia o la conectividad de la red.

Una vez que se restablece la conectividad, su aplicación recibe el conjunto apropiado de eventos para que el cliente se sincronice con el estado actual del servidor, sin tener que escribir ningún código personalizado.

Hablaremos más sobre el comportamiento fuera de línea en Más información sobre las capacidades en línea y fuera de línea .

Próximos pasos