Lee y escribe datos en la Web

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

Antes de analizar cómo la app realiza operaciones de lectura y escritura en Realtime Database, veremos Firebase Local Emulator Suite, un conjunto de herramientas con el que puedes crear prototipos y probar la funcionalidad 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 Local Emulator Suite.

Obtén una referencia a una base de datos

Para leer o escribir en la base de datos, necesitas una instancia de firebase.database.Reference:

API modular web

import { getDatabase } from "firebase/database";

const database = getDatabase();

API con espacio de nombres web

var database = firebase.database();

Escribe datos

En este documento, se abarcan los conceptos básicos de la recuperación de datos y cómo ordenarlos y filtrarlos en Firebase.

Para recuperar los datos de Firebase, se debe adjuntar un objeto de escucha asíncrono a firebase.database.Reference. El objeto de escucha se activa una vez para el estado inicial de los datos y nuevamente cada vez que 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 en esa ruta de acceso. Por ejemplo, una aplicación social de blogs puede agregar un usuario con set() de la siguiente manera:

API modular web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

API con espacio de nombres web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

El uso de set() reemplaza los datos en la ubicación especificada, incluidos los nodos secundarios.

Lee datos

Detecta eventos de valores

Para leer datos de una ruta de acceso y detectar cambios, usa onValue() a fin de observar los eventos. Puedes usar este evento para leer instantáneas estáticas del contenido de una ruta de acceso determinada y ver cómo se encontraba en el momento del evento. Este método se inicia cuando se adjunta el objeto de escucha y nuevamente cada vez que cambian los datos, incluidos los secundarios. La devolución de llamada del evento recibe una instantánea que contiene todos los datos de esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea mostrará el valor false cuando llames a exists() y null cuando llames a val().

En el siguiente ejemplo, se demuestra una aplicación social de blogs que recupera el recuento de estrellas de una entrada desde la base de datos:

API modular web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

API con espacio de nombres web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

El objeto de escucha recibe una snapshot que contiene los datos de la ubicación específica en la base de datos en el momento en que ocurrió el evento. Puedes recuperar los datos de la snapshot con el método val().

Lee los datos una sola vez

Lee datos 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. Las técnicas de los objetos de escucha 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.

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

API modular web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

API con espacio de nombres web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Lee datos una sola vez con un observador

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 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:

API modular web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

API con espacio de nombres web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().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 reemplazar 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 cree una entrada y que esta se actualice de forma simultánea en el feed de actividad reciente y en el feed de actividad de publicaciones del usuario con un código como el siguiente:

API modular web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

API con espacio de nombres web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().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 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 agregar una devolución de llamada de finalización. Tanto set() como update() toman una devolución de llamada opcional de finalización que se llama cuando la escritura se confirma en la base de datos. Si la llamada no funciona correctamente, la devolución de llamada recibirá un objeto de error que indicará el motivo.

API modular web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

API con espacio de nombres web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

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 null como el valor de otra operación de escritura, como set() o update(). Puedes usar esta técnica con update() para borrar varios datos secundarios con una sola llamada a la API.

Recibe una Promise

Para saber cuándo se confirman tus datos en el servidor de Firebase Realtime Database, puedes usar una Promise. Tanto set() como update() pueden mostrar una Promise que puedes usar para saber cuándo se confirma la escritura en la base de datos.

Desvincula objetos de escucha

Para quitar las devoluciones de llamada, llama al método off() en tu referencia de la base de datos de Firebase.

Para quitar un solo objeto de escucha, puedes pasarlo como parámetro a off(). Si llamas a off() en la ubicación que no tiene argumentos, se quitarán todos los objetos de escucha de esa ubicación.

Si llamas a off() en un objeto de escucha primario, no se quitan automáticamente los objetos de escucha registrados en sus nodos secundarios. Deberás llamar a off() también en todos los objetos de escucha secundarios para quitar la devolución de llamada.

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 usar una operación de transacción. Esta operación acepta una función de actualización y una devolución de llamada opcional de finalización. La función de actualización toma el estado actual de los datos como argumento y genera el nuevo estado que deseas escribir. Si otro cliente escribe en la ubicación antes de que se escriba de manera correcta el valor nuevo, se vuelve a llamar a la función de actualización con el nuevo valor actual y se intenta nuevamente la operación de escritura.

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

API modular web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

API con espacio de nombres web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Cuando usas una transacción, evitas que el recuento de estrellas sea incorrecto en caso de que varios usuarios destaquen la entrada al mismo tiempo o el cliente tenga datos inactivos. Si se rechaza la transacción, el servidor le muestra el valor actual al cliente, que vuelve a ejecutar la transacción con el valor actualizado. Esto se repite hasta que se acepte la transacción, o hasta que decidas anularla.

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.

API modular web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

API con espacio de nombres web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().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.

Encontrarás 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