Saltar a contenido

Capítulo 10 — Operaciones CRUD en Cloud Firestore

Recursos visuales propuestos

Antes de desarrollar este capítulo conviene seleccionar únicamente los recursos visuales que mejoren de verdad la comprensión del lector. En operaciones CRUD, la mayoría de conceptos iniciales se entienden mejor con secuencias simples, comparaciones antes-después y representaciones pedagógicas de documentos. Los diagramas SVG deben reservarse para los casos donde sea necesario mostrar el flujo técnico interno entre la aplicación, el SDK y Firestore.

Imágenes didácticas

  1. Ciclo CRUD. Se recomienda como imagen didáctica porque resume de forma muy clara las cuatro operaciones fundamentales y ayuda a fijar la lógica general del capítulo.
  2. Ejemplos visuales de documentos antes y después de una actualización. Conviene como imagen didáctica porque permite mostrar la diferencia entre sobrescribir, hacer merge o actualizar un campo sin obligar al lector a interpretar solo código.[cite:1]
  3. Comparación entre Create, Update y Set. Es mejor como imagen didáctica porque el valor pedagógico está en contrastar comportamiento y efectos sobre un documento existente o inexistente.[cite:1]
  4. Flujo básico de lectura y escritura. También conviene como imagen didáctica porque ayuda a entender el ciclo funcional de la aplicación sin caer todavía en diagramas demasiado técnicos.

Diagramas SVG

  1. Flujo interno entre aplicación, SDK y Firestore. Aquí sí conviene SVG porque el objetivo es mostrar una secuencia técnica entre componentes distintos: interfaz, SDK modular, caché local, red y backend de Firestore.
  2. Secuencia completa de una operación CRUD. Se recomienda como SVG porque permite representar con precisión qué ocurre durante una escritura o lectura, incluyendo respuesta, sincronización y posibles errores.[cite:1]
  3. Comunicación entre cliente, Firestore y sincronización en tiempo real. Debe ser SVG porque implica eventos, confirmación remota y actualización del estado del cliente, algo que una imagen sencilla suele simplificar demasiado.

Objetivos de aprendizaje

Al finalizar este capítulo, el lector será capaz de:

  • Comprender qué significa CRUD en el contexto de Cloud Firestore y cómo se relaciona con las operaciones fundamentales de una aplicación moderna.
  • Crear, leer, actualizar y eliminar documentos utilizando la sintaxis modular moderna del SDK de Firebase para web.[cite:1]
  • Diferenciar con precisión entre crear con ID automático, crear con ID personalizado, sobrescribir con setDoc(), hacer merge parcial con setDoc(..., { merge: true }) y actualizar campos específicos con updateDoc().[cite:1]
  • Entender por qué todas las operaciones con Firestore son asíncronas y cómo manejarlas correctamente con Promises y async/await.[cite:1]
  • Evaluar el impacto de las operaciones CRUD sobre costo, rendimiento, claridad del código y experiencia de usuario.[cite:2]
  • Aplicar buenas prácticas profesionales sobre validación, manejo de errores, escrituras parciales y organización del acceso a datos.

Introducción

CRUD es el acrónimo de Create, Read, Update y Delete. En cualquier sistema que gestione información, estas cuatro operaciones representan el núcleo de la interacción con la base de datos. En Firestore no son una excepción, pero sí tienen matices importantes: cada operación tiene implicaciones concretas sobre latencia, costo, cantidad de lecturas y escrituras, consistencia del modelo y mantenimiento del código.

La documentación oficial de Firestore organiza estas capacidades en guías como Add data, Get data y Transactions and batched writes, donde muestra que el SDK permite crear documentos, agregarlos con IDs automáticos, sobrescribir datos existentes, hacer merges parciales y ejecutar operaciones atómicas sobre múltiples documentos.[cite:1][cite:3] Desde el punto de vista del desarrollo profesional, conocer la sintaxis no basta. El verdadero dominio comienza cuando el desarrollador entiende qué efecto produce cada llamada y cuándo conviene usar una u otra.

Además, Firestore no solo cobra “por usar la base”. Cobra por operaciones concretas y por volumen procesado. La documentación de ejemplos de precios muestra que las lecturas consumen unidades calculadas en tramos de 4 KiB y que las escrituras se calculan en tramos de 1 KiB, incluyendo el impacto de índices y del tamaño del documento.[cite:2] Por eso, aprender CRUD en Firestore no consiste solo en “guardar datos”. Consiste en manipular datos con criterio técnico y económico.

En este capítulo se trabajará exclusivamente con la sintaxis modular moderna del SDK de Firebase para web, v9 o superior, evitando por completo el estilo obsoleto basado en namespaces. Todos los ejemplos se relacionarán con el proyecto oficial del libro para que el aprendizaje tenga continuidad arquitectónica y funcional.

Desarrollo completo

¿Qué es CRUD?

CRUD es una forma de clasificar las operaciones mínimas que casi toda aplicación necesita sobre sus datos:

  • Create: crear información nueva.
  • Read: leer información existente.
  • Update: modificar información ya almacenada.
  • Delete: eliminar información.

En Firestore, estas cuatro operaciones se aplican principalmente sobre documentos. Eso significa que no se “crea una tabla” ni se “actualiza una fila” en sentido relacional. Se crean, leen, actualizan y eliminan documentos ubicados en colecciones y, si existe una relación jerárquica, en subcolecciones.[cite:1]

A nivel arquitectónico, esta diferencia importa mucho. El documento es la unidad de trabajo y, por tanto, también la unidad de costo y sincronización más visible para la aplicación. Un CRUD mal diseñado puede incrementar lecturas, disparar escrituras innecesarias o sobrescribir datos que no debían tocarse.

Operaciones disponibles en Firestore

La documentación oficial sobre escritura de datos indica que se puede escribir en Firestore de varias maneras: establecer datos en un documento con ID explícito, agregar un nuevo documento a una colección con ID generado automáticamente, crear un documento vacío con ID automático y asignarle datos después, actualizar datos y usar operaciones atómicas para escrituras múltiples.[cite:1][cite:3]

En la práctica, para el SDK web modular, las funciones básicas más importantes de este capítulo son:

  • collection()
  • doc()
  • addDoc()
  • setDoc()
  • getDoc()
  • getDocs()
  • updateDoc()
  • deleteDoc()
  • writeBatch()
  • runTransaction()

Cada una resuelve un problema distinto. Parte del dominio profesional consiste en no usarlas como si fueran intercambiables.

Configuración base del SDK modular

Antes de operar con Firestore, se necesita inicializar Firebase y obtener una instancia de la base de datos. La documentación oficial muestra esta forma para el SDK web moderno:[cite:1]

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: 'TU_API_KEY',
  authDomain: 'TU_DOMINIO.firebaseapp.com',
  projectId: 'tu-project-id',
  storageBucket: 'tu-project-id.appspot.com',
  messagingSenderId: '1234567890',
  appId: '1:1234567890:web:abcdef123456'
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

Este bloque no realiza todavía operaciones CRUD. Solo prepara el contexto de trabajo. Internamente, getFirestore(app) devuelve la instancia del servicio Firestore asociada a la aplicación actual.[cite:1]

Crear documentos

Crear un documento significa escribir por primera vez información en una ubicación determinada de la base. En Firestore hay varias formas de hacerlo, y no todas responden a la misma intención arquitectónica.[cite:1]

La primera distinción importante es esta:

  • Crear especificando el ID del documento.
  • Crear dejando que Firestore genere automáticamente el ID.

Ambas son válidas. La elección depende de si el documento tiene una identidad natural estable o si solo se necesita una clave única generada por la plataforma.

Crear documentos con ID automático

Cuando no existe una identidad natural fuerte, lo más conveniente suele ser usar addDoc(). Esta función agrega un nuevo documento dentro de una colección y deja que Firestore genere el identificador.[cite:1]

import { collection, addDoc, serverTimestamp } from 'firebase/firestore';

async function crearCurso() {
  const cursosRef = collection(db, 'cursos');

  const nuevoCursoRef = await addDoc(cursosRef, {
    titulo: 'Matemáticas Aplicadas I',
    descripcion: 'Curso introductorio para primer semestre.',
    institucionId: 'inst_centro_digital',
    docenteId: 'uid_docente_001',
    estado: 'borrador',
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });

  return nuevoCursoRef.id;
}

La ventaja principal es la simplicidad. Firestore genera un ID único y el documento queda creado en la colección indicada.[cite:1] Este patrón es ideal para entidades como cursos, tareas, evidencias o archivos, donde la aplicación no necesita controlar manualmente el identificador.

Crear documentos con ID personalizado

Cuando sí existe una identidad natural o conviene mantener una clave conocida, se utiliza setDoc() junto con doc() para apuntar a una ubicación específica.[cite:1]

import { doc, setDoc, serverTimestamp } from 'firebase/firestore';

async function crearUsuario(uid, datosUsuario) {
  const usuarioRef = doc(db, 'usuarios', uid);

  await setDoc(usuarioRef, {
    displayName: datosUsuario.displayName,
    email: datosUsuario.email,
    rol: 'docente',
    activo: true,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });
}

En este ejemplo, el documento del usuario queda en usuarios/{uid}. Este patrón es especialmente apropiado cuando se quiere alinear Firestore con el UID generado por Authentication.

Agregar documentos a una colección

En la práctica, “agregar documentos a una colección” suele equivaler a usar addDoc() sobre la referencia de esa colección.[cite:1] Sin embargo, también puede hacerse con setDoc(doc(collectionRef)), es decir, creando primero una referencia con ID automático y luego escribiendo en ella.

import { collection, doc, setDoc, serverTimestamp } from 'firebase/firestore';

async function crearTarea() {
  const tareasRef = collection(db, 'tareas');
  const tareaRef = doc(tareasRef);

  await setDoc(tareaRef, {
    titulo: 'Resolver ejercicios de álgebra',
    cursoId: 'curso_abc123',
    claseId: 'clase_01',
    fechaEntrega: '2026-07-15',
    estado: 'publicada',
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });

  return tareaRef.id;
}

Esta variante es útil cuando se necesita conocer la referencia o el ID antes de completar la escritura, por ejemplo para relacionar otros documentos o construir rutas derivadas.

Leer un documento

Para leer un solo documento se utiliza getDoc() sobre una referencia creada con doc().[cite:1]

import { doc, getDoc } from 'firebase/firestore';

async function obtenerCurso(cursoId) {
  const cursoRef = doc(db, 'cursos', cursoId);
  const cursoSnap = await getDoc(cursoRef);

  if (!cursoSnap.exists()) {
    throw new Error('El curso no existe');
  }

  return {
    id: cursoSnap.id,
    ...cursoSnap.data()
  };
}

Internamente, getDoc() resuelve una operación asíncrona y devuelve un DocumentSnapshot. Ese snapshot contiene metadatos, el ID y la información del documento si existe.[cite:1] Un punto importante es que el hecho de tener una referencia con doc() no significa que el documento exista. La existencia solo se confirma al leerlo.[cite:1]

Leer múltiples documentos

Para leer varios documentos desde una colección se usa getDocs() sobre una referencia de colección o sobre una consulta. Aunque las consultas avanzadas se estudiarán más adelante, ya es importante entender la mecánica básica de lectura múltiple.

import { collection, getDocs } from 'firebase/firestore';

async function obtenerInstituciones() {
  const institucionesRef = collection(db, 'instituciones');
  const snapshot = await getDocs(institucionesRef);

  return snapshot.docs.map(docSnap => ({
    id: docSnap.id,
    ...docSnap.data()
  }));
}

Cada documento recuperado implica trabajo de lectura. La documentación de precios muestra que las lecturas consumen unidades según el volumen procesado, en tramos de 4 KiB, y que una lectura puntual de un documento pequeño consume una unidad de lectura.[cite:2] Esto hace que “leer todo” sin pensarlo nunca sea una buena práctica por defecto.

Obtener todos los documentos de una colección

Obtener todos los documentos de una colección es técnicamente sencillo con getDocs(collection(db, 'nombreColeccion')), pero arquitectónicamente debe usarse con cautela.

import { collection, getDocs } from 'firebase/firestore';

async function obtenerTodosLosGrupos() {
  const gruposRef = collection(db, 'grupos');
  const gruposSnap = await getDocs(gruposRef);

  return gruposSnap.docs.map(item => ({
    id: item.id,
    ...item.data()
  }));
}

Este patrón es razonable para colecciones pequeñas o paneles muy controlados. En una aplicación profesional, conviene asumir que muchas colecciones crecerán. Leer todos los documentos de manera indiscriminada puede elevar latencia, ancho de banda y costo.[cite:2]

Actualizar documentos completos

En Firestore, “actualizar completamente” un documento no siempre se expresa con una función llamada update. Si se usa setDoc() sobre un documento existente sin opción de merge, el contenido previo se sobrescribe con el nuevo objeto.[cite:1]

import { doc, setDoc, serverTimestamp } from 'firebase/firestore';

async function reemplazarConfiguracionSistema() {
  const configRef = doc(db, 'configuraciones', 'sistema');

  await setDoc(configRef, {
    mantenimiento: false,
    versionMinimaApp: '1.1.0',
    modulosActivos: {
      tareas: true,
      credenciales: true,
      notificaciones: true
    },
    updatedAt: serverTimestamp()
  });
}

Esta operación es útil cuando realmente se quiere definir el documento completo. Pero también es peligrosa si el desarrollador cree estar cambiando solo unos campos y, sin darse cuenta, elimina otros. Aquí aparece una de las diferencias más importantes del capítulo: crear o sobrescribir no es lo mismo que actualizar parcialmente.[cite:1]

Actualizar campos específicos

Para modificar solo ciertos campos de un documento existente se utiliza updateDoc().[cite:1]

import { doc, updateDoc, serverTimestamp } from 'firebase/firestore';

async function publicarCurso(cursoId) {
  const cursoRef = doc(db, 'cursos', cursoId);

  await updateDoc(cursoRef, {
    estado: 'publicado',
    updatedAt: serverTimestamp()
  });
}

updateDoc() está pensado para cambios parciales. A diferencia de setDoc(), no reemplaza el documento completo. Y, a diferencia de setDoc(..., { merge: true }), normalmente se usa cuando se asume que el documento ya existe. Esta diferencia semántica es muy valiosa porque expresa intención en el código.

Eliminar documentos

Para borrar un documento se utiliza deleteDoc().

import { doc, deleteDoc } from 'firebase/firestore';

async function eliminarArchivo(archivoId) {
  const archivoRef = doc(db, 'archivos', archivoId);
  await deleteDoc(archivoRef);
}

Eliminar un documento parece simple, pero la decisión debe tomarse con cuidado. Borrar datos puede afectar reglas de negocio, integridad funcional y experiencia del usuario. Además, la documentación de precios muestra que las eliminaciones también consumen unidades de escritura y que su costo puede verse afectado por los índices asociados al documento.[cite:2]

Eliminar campos

No siempre se necesita borrar un documento completo. En muchos casos basta con eliminar un campo específico. En el SDK modular esto se hace con updateDoc() y el valor sentinela deleteField().

import { doc, updateDoc, deleteField } from 'firebase/firestore';

async function eliminarFotoPerfil(uid) {
  const usuarioRef = doc(db, 'usuarios', uid);

  await updateDoc(usuarioRef, {
    photoURL: deleteField()
  });
}

Esta operación es útil cuando el documento debe conservarse pero un atributo ya no tiene sentido. Por ejemplo, eliminar un campo temporal, una miniatura obsoleta o un bloque de metadatos que ya no debe persistir.

Operaciones asíncronas

Todas las operaciones principales de Firestore desde el cliente son asíncronas.[cite:1] Esto ocurre porque implican acceso a recursos externos, coordinación con la caché local, validación mediante reglas y eventual sincronización con el backend.

El hecho de que una operación sea asíncrona significa que el código no puede comportarse como si Firestore respondiera de forma inmediata y bloqueante. La app debe esperar el resultado, capturar errores y reflejar correctamente estados como “guardando”, “cargando” o “reintentando”.

Desde el punto de vista pedagógico, este es uno de los cambios más importantes para quien viene de ejemplos muy simples en JavaScript. En Firestore, ignorar la asincronía casi siempre produce bugs.

Manejo de Promises

Las funciones del SDK web moderno devuelven Promises. Eso significa que pueden usarse con .then() y .catch().

import { doc, setDoc } from 'firebase/firestore';

function crearInstitucionConPromises() {
  const institucionRef = doc(db, 'instituciones', 'inst_demo');

  return setDoc(institucionRef, {
    nombre: 'Institución Demo',
    activa: true
  })
    .then(() => {
      console.log('Institución creada correctamente');
    })
    .catch(error => {
      console.error('Error al crear institución:', error);
      throw error;
    });
}

Este estilo sigue siendo válido, especialmente para explicar cómo funcionan las Promises. Sin embargo, en aplicaciones modernas suele preferirse async/await por claridad de lectura.

Uso de async/await

async/await hace que el flujo asíncrono se lea de forma más cercana a una secuencia lógica tradicional.

import { collection, addDoc, serverTimestamp } from 'firebase/firestore';

async function registrarEvidencia(datos) {
  try {
    const evidenciasRef = collection(db, 'evidencias');

    const evidenciaRef = await addDoc(evidenciasRef, {
      tareaId: datos.tareaId,
      alumnoId: datos.alumnoId,
      cursoId: datos.cursoId,
      estado: 'entregada',
      comentario: datos.comentario,
      submittedAt: serverTimestamp()
    });

    return evidenciaRef.id;
  } catch (error) {
    console.error('No fue posible registrar la evidencia', error);
    throw error;
  }
}

A nivel profesional, async/await facilita el manejo de errores, la composición de flujos y la lectura del código. Por eso será el estilo predominante en este capítulo.

Manejo de errores

Ninguna operación sobre la base debe darse por garantizada. Una escritura puede fallar por reglas de seguridad, conectividad, datos inválidos o referencias mal construidas. Una lectura puede fallar por permisos, red o lógica de negocio.

Un patrón profesional mínimo es envolver operaciones críticas en try/catch y devolver mensajes controlados a la interfaz:

async function actualizarEstadoTarea(tareaId, nuevoEstado) {
  try {
    const tareaRef = doc(db, 'tareas', tareaId);

    await updateDoc(tareaRef, {
      estado: nuevoEstado,
      updatedAt: serverTimestamp()
    });

    return { ok: true };
  } catch (error) {
    console.error('Error al actualizar la tarea', error);
    return {
      ok: false,
      message: 'No se pudo actualizar la tarea.'
    };
  }
}

La aplicación no debe exponer directamente errores técnicos sin contexto. Debe registrarlos para diagnóstico y traducirlos a mensajes útiles para el usuario.

Validación antes de guardar

Firestore no sustituye la validación de datos de la aplicación. Antes de escribir, el cliente debe validar estructura mínima, tipos, rangos y consistencia de negocio. Después, las reglas de seguridad complementarán esa protección desde el lado del backend.

Ejemplo sencillo de validación previa:

function validarCurso(datos) {
  if (!datos.titulo || datos.titulo.trim().length < 5) {
    throw new Error('El título del curso debe tener al menos 5 caracteres.');
  }

  if (!datos.docenteId) {
    throw new Error('El curso debe estar asociado a un docente.');
  }
}

async function guardarCurso(datos) {
  validarCurso(datos);

  const cursosRef = collection(db, 'cursos');
  await addDoc(cursosRef, {
    ...datos,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });
}

Validar antes de guardar evita escrituras inútiles, mejora experiencia de usuario y reduce costos derivados de operaciones fallidas o datos defectuosos.

Escrituras parciales

Una escritura parcial es aquella que solo modifica una parte del documento existente. En Firestore hay dos estrategias principales para lograrla:

  • updateDoc() para cambiar campos específicos.
  • setDoc() con { merge: true } para combinar el nuevo contenido con el ya existente.[cite:1]

La elección depende de la intención. Si el documento debe existir y se quiere expresar una actualización puntual, updateDoc() suele ser más semántico. Si no se sabe si existe y se quiere crear o completar sin sobrescribir el resto, setDoc(..., { merge: true }) puede ser mejor.[cite:1]

Sobrescritura de documentos

La sobrescritura ocurre cuando se utiliza setDoc() sin merge sobre una referencia y el documento ya existe. En ese caso, el contenido previo se reemplaza por el nuevo objeto.[cite:1]

import { doc, setDoc } from 'firebase/firestore';

async function sobrescribirPerfilAlumno(alumnoId) {
  const alumnoRef = doc(db, 'alumnos', alumnoId);

  await setDoc(alumnoRef, {
    nombreCompleto: 'Carlos Herrera',
    matricula: 'A-2026-009',
    grado: '1A'
  });
}

Si antes había otros campos como fotoURL, telefono o observaciones, desaparecerán salvo que se vuelvan a incluir. Esta es una fuente clásica de errores.

Merge de documentos

La documentación oficial muestra que setDoc() admite la opción { merge: true } para fusionar el nuevo contenido con el ya existente.[cite:1]

import { doc, setDoc, serverTimestamp } from 'firebase/firestore';

async function completarConfiguracionUsuario(uid) {
  const usuarioRef = doc(db, 'usuarios', uid);

  await setDoc(
    usuarioRef,
    {
      settings: {
        theme: 'dark',
        language: 'es-MX'
      },
      updatedAt: serverTimestamp()
    },
    { merge: true }
  );
}

Esto es útil cuando no se quiere reemplazar el documento completo. No obstante, hay que conocer un detalle importante señalado en la documentación: si se hace un set con merge y uno de los campos es un mapa vacío, ese campo de mapa en el documento objetivo se sobrescribe.[cite:1] Por eso, merge no significa “inteligencia total”; significa combinación controlada con reglas concretas.

Operaciones atómicas

Cuando varias escrituras deben ejecutarse como una sola unidad lógica, Firestore ofrece operaciones atómicas, principalmente transacciones y lotes de escritura (batched writes).[cite:3]

Las operaciones atómicas son importantes porque permiten coordinar cambios relacionados sin dejar la base en estados intermedios inconsistentes. Por ejemplo, crear una tarea y actualizar un contador relacionado, o registrar una entrega y ajustar un resumen.

Lotes de escritura

Los lotes de escritura permiten agrupar múltiples escrituras y confirmarlas juntas.[cite:3]

import { writeBatch, doc, serverTimestamp } from 'firebase/firestore';

async function crearCursoYConfiguracion(cursoId, datosCurso) {
  const batch = writeBatch(db);

  const cursoRef = doc(db, 'cursos', cursoId);
  const configRef = doc(db, 'configuracionesCursos', cursoId);

  batch.set(cursoRef, {
    ...datosCurso,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });

  batch.set(configRef, {
    visibilidad: 'privada',
    permitirComentarios: true,
    updatedAt: serverTimestamp()
  });

  await batch.commit();
}

Los lotes son ideales cuando ya se conocen los valores a escribir y no hace falta leer primero para decidir el cambio.

Transacciones

Las transacciones son preferibles cuando la lógica depende de leer primero y escribir después según el valor actual.[cite:3]

import { doc, runTransaction, serverTimestamp } from 'firebase/firestore';

async function incrementarIntentosRevision(tareaId) {
  const tareaRef = doc(db, 'tareas', tareaId);

  await runTransaction(db, async transaction => {
    const tareaSnap = await transaction.get(tareaRef);

    if (!tareaSnap.exists()) {
      throw new Error('La tarea no existe.');
    }

    const intentosActuales = tareaSnap.data().intentosRevision ?? 0;

    transaction.update(tareaRef, {
      intentosRevision: intentosActuales + 1,
      updatedAt: serverTimestamp()
    });
  });
}

La documentación oficial presenta transacciones y batched writes como herramientas clave para escrituras masivas o coordinadas.[cite:3] Aunque su estudio profundo puede ampliarse más adelante, desde este capítulo ya conviene entender su lugar dentro del CRUD profesional.

Costos asociados a cada operación

Las operaciones CRUD tienen impacto económico medible. La documentación de ejemplos de precios de Firestore detalla varios casos muy útiles:

  • Una lectura puntual de un documento de 1 KiB consume 1 unidad de lectura.[cite:2]
  • Leer 100 documentos de 1 KiB consume 100 unidades de lectura.[cite:2]
  • Un documento de 1 MiB leído como punto consume 256 unidades de lectura.[cite:2]
  • Las escrituras se calculan en tramos de 1 KiB y consideran también índices.[cite:2]
  • Una actualización puede costar más si afecta entradas de índice, porque actualizar un índice implica eliminar y recrear entradas.[cite:2]
  • Una eliminación también consume unidades de escritura y puede reflejar el costo de índices asociados.[cite:2]

Estas observaciones cambian la forma de diseñar la aplicación. No es lo mismo leer un documento pequeño que cien documentos medianos. No es lo mismo actualizar un documento por ID que lanzar una operación que primero consulta muchos documentos para después modificarlos.

Optimización de lecturas

Optimizar lecturas en Firestore significa reducir la cantidad de documentos recuperados, el tamaño de cada documento y el número de veces que la aplicación los solicita innecesariamente.

Buenas prácticas concretas:

  • Leer un documento específico cuando ya se conoce su ID.
  • Evitar getDocs() sobre colecciones enteras si la interfaz solo necesita un subconjunto.
  • Diseñar documentos compactos para pantallas frecuentes.
  • No volver a leer el mismo recurso si ya está en memoria y no necesita refresco inmediato.
  • Distinguir entre vistas de resumen y vistas de detalle.

La documentación de precios deja claro que el tamaño y volumen de lectura importan económicamente.[cite:2] Por eso, optimizar lecturas no es microoptimización: es una decisión de arquitectura.

Optimización de escrituras

Optimizar escrituras significa evitar cambios innecesarios, reducir sobrescrituras completas cuando bastan actualizaciones parciales y no disparar procesos de escritura redundantes.

Algunas recomendaciones:

  • Preferir updateDoc() para cambios específicos.
  • Usar setDoc() completo solo cuando realmente se quiere reemplazar el documento.
  • Validar antes de escribir para no enviar datos defectuosos.
  • No hacer escrituras “en cada pulsación” salvo que el caso de uso lo justifique.
  • Diseñar bien los documentos para no tener que reescribir bloques gigantes por un cambio pequeño.

La documentación de precios ilustra que incluso una actualización sin cambios puede generar costo mínimo de escritura.[cite:2] Eso significa que escribir “por si acaso” nunca es gratis.

Ejemplos paso a paso

Ejemplo 1: crear un usuario del proyecto

import { doc, setDoc, serverTimestamp } from 'firebase/firestore';

async function registrarDocente(uid, payload) {
  const usuarioRef = doc(db, 'usuarios', uid);

  await setDoc(usuarioRef, {
    displayName: payload.displayName,
    email: payload.email,
    rol: 'docente',
    activo: true,
    institucionId: payload.institucionId,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });
}

Por qué así: el UID de Authentication es una identidad natural fuerte. Usarlo como ID facilita la relación entre autenticación y base de datos.

Ejemplo 2: crear un curso con ID automático

import { collection, addDoc, serverTimestamp } from 'firebase/firestore';

async function crearCursoDemo() {
  const cursosRef = collection(db, 'cursos');

  const cursoRef = await addDoc(cursosRef, {
    titulo: 'Física General I',
    descripcion: 'Curso base para estudiantes de primer nivel.',
    docenteId: 'uid_docente_001',
    institucionId: 'inst_centro_digital',
    estado: 'borrador',
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });

  return cursoRef.id;
}

Por qué así: cursos no requiere normalmente un ID semántico estable. El Auto ID es suficiente y conveniente.[cite:1]

Ejemplo 3: leer una tarea concreta

import { doc, getDoc } from 'firebase/firestore';

async function obtenerTarea(tareaId) {
  const tareaRef = doc(db, 'tareas', tareaId);
  const tareaSnap = await getDoc(tareaRef);

  if (!tareaSnap.exists()) {
    return null;
  }

  return {
    id: tareaSnap.id,
    ...tareaSnap.data()
  };
}

Por qué así: cuando se conoce el ID, una lectura puntual suele ser la opción más directa y económica para recuperar un único documento.[cite:2]

Ejemplo 4: actualizar solo el estado de una evidencia

import { doc, updateDoc, serverTimestamp } from 'firebase/firestore';

async function marcarEvidenciaRevisada(evidenciaId) {
  const evidenciaRef = doc(db, 'evidencias', evidenciaId);

  await updateDoc(evidenciaRef, {
    estado: 'revisada',
    reviewedAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });
}

Por qué así: se modifican campos puntuales y no todo el documento. Esto expresa mejor la intención y evita sobrescrituras accidentales.

Ejemplo 5: completar configuración con merge

import { doc, setDoc } from 'firebase/firestore';

async function inicializarConfiguracionUsuario(uid) {
  const usuarioRef = doc(db, 'usuarios', uid);

  await setDoc(
    usuarioRef,
    {
      settings: {
        theme: 'dark',
        pushEnabled: true
      }
    },
    { merge: true }
  );
}

Por qué así: esta operación agrega o combina configuración sin reemplazar el resto del documento.[cite:1]

Ejemplo 6: eliminar una credencial obsoleta

import { doc, deleteDoc } from 'firebase/firestore';

async function eliminarCredencial(credencialId) {
  const credencialRef = doc(db, 'credenciales', credencialId);
  await deleteDoc(credencialRef);
}

Por qué así: cuando el recurso completo ya no tiene valor operativo, eliminar el documento es más claro que dejarlo semivacío.

Comparaciones entre métodos

addDoc() vs setDoc()

Método Cuándo usarlo Ventaja principal Riesgo principal
addDoc() Cuando no se necesita controlar el ID.[cite:1] Simplicidad y Auto ID. Menor legibilidad si luego se quería un ID significativo.
setDoc() Cuando se conoce o se decide el ID.[cite:1] Control total de la ruta. Puede sobrescribir por completo si no se usa merge.

setDoc() vs updateDoc()

Método Comportamiento Cuándo conviene
setDoc() Crea o sobrescribe el documento completo.[cite:1] Cuando se quiere definir el documento completo.
setDoc(..., { merge: true }) Fusiona campos con el documento existente.[cite:1] Cuando no se quiere reemplazar todo o no se sabe si existe.
updateDoc() Cambia campos específicos en un documento existente. Cuando se expresan cambios parciales precisos.

Sobrescribir vs merge

Supongamos que el documento original es este:

{
  "displayName": "Laura Mendoza",
  "email": "laura@institucion.edu",
  "rol": "docente",
  "settings": {
    "theme": "light",
    "pushEnabled": true
  }
}

Si se hace esto:

await setDoc(usuarioRef, {
  settings: { theme: 'dark' }
});

el documento se reemplaza y se pierden displayName, email, rol y el resto del contenido.

Si se hace esto otro:

await setDoc(usuarioRef, {
  settings: { theme: 'dark' }
}, { merge: true });

se preserva el resto del documento y solo se fusiona la nueva estructura.[cite:1]

Buenas prácticas

  • Usar siempre la sintaxis modular moderna del SDK, no namespaces obsoletos.[cite:1]
  • Elegir el método según la intención: addDoc() para Auto ID, setDoc() para definir un documento, updateDoc() para cambios parciales.
  • Validar datos antes de escribir.
  • Manejar errores con try/catch y mensajes controlados.
  • Diseñar documentos pequeños para que las lecturas sean más eficientes.[cite:2]
  • Evitar leer colecciones completas cuando la interfaz solo necesita una parte.
  • No sobrescribir documentos completos si basta con actualizar campos concretos.
  • Usar merge cuando se quiera crear o completar sin destruir el documento existente.[cite:1]
  • Emplear lotes o transacciones cuando varias escrituras deban comportarse de forma coordinada.[cite:3]
  • Pensar siempre en costo y frecuencia de operación, no solo en que “funcione”.[cite:2]

Errores comunes

  • Usar setDoc() creyendo que actualiza parcialmente cuando en realidad sobrescribe todo.[cite:1]
  • Leer todos los documentos de una colección por comodidad, aunque la vista solo necesite uno o unos pocos.[cite:2]
  • No validar datos antes de guardar.
  • Ignorar la asincronía y usar resultados antes de que la Promise se resuelva.
  • No manejar errores de red o permisos.
  • Actualizar documentos repetidamente sin necesidad funcional real.
  • Elegir IDs personalizados demasiado largos o inestables.
  • Usar operaciones atómicas demasiado tarde, cuando ya se introdujo inconsistencia entre documentos relacionados.

Resumen

Las operaciones CRUD en Cloud Firestore son la base de todo trabajo real con datos: crear, leer, actualizar y eliminar documentos dentro de colecciones y subcolecciones. Sin embargo, dominar CRUD en Firestore exige ir más allá de la sintaxis y comprender que cada llamada expresa una intención distinta y produce efectos diferentes sobre el documento, la aplicación y el costo operativo.[cite:1][cite:2]

addDoc() es adecuado cuando se desea un ID automático, setDoc() permite crear o sobrescribir un documento en una ruta concreta, setDoc(..., { merge: true }) sirve para fusionar datos sin reemplazar por completo, y updateDoc() es la opción natural para cambios parciales.[cite:1] Además, las lecturas, escrituras y eliminaciones tienen impacto económico medible, calculado según tamaño de documentos e índices, por lo que optimizar CRUD también es optimizar arquitectura.[cite:2]

Un uso profesional de Firestore combina código claro, validación previa, manejo correcto de Promises, async/await, control de errores y decisiones conscientes sobre qué leer, qué escribir y cuándo hacerlo. Sobre esa base se construirán los próximos capítulos, donde las operaciones CRUD se integrarán con consultas, seguridad, tiempo real y lógica de aplicación más avanzada.

Conceptos clave

  • CRUD.
  • Documento.
  • Colección.
  • addDoc().[cite:1]
  • setDoc().[cite:1]
  • updateDoc().
  • deleteDoc().
  • deleteField().
  • Merge de documentos.[cite:1]
  • Sobrescritura de documentos.[cite:1]
  • Promise.
  • async/await.
  • Operación atómica.[cite:3]
  • writeBatch().[cite:3]
  • runTransaction().[cite:3]
  • Costo por lectura.[cite:2]
  • Costo por escritura.[cite:2]
  • Optimización de lecturas.
  • Optimización de escrituras.

Preguntas de repaso

  1. ¿Qué significa CRUD en el contexto de Firestore?
  2. ¿Cuándo conviene usar addDoc() y cuándo setDoc()?[cite:1]
  3. ¿Qué diferencia hay entre sobrescribir un documento y hacer merge parcial?[cite:1]
  4. ¿Por qué updateDoc() expresa mejor una actualización puntual que setDoc() sin merge?
  5. ¿Qué riesgos existen al leer todos los documentos de una colección sin necesidad?[cite:2]
  6. ¿Por qué las operaciones de Firestore son asíncronas?[cite:1]
  7. ¿Qué ventajas tiene async/await frente a encadenar múltiples .then()?
  8. ¿Cómo afectan tamaño de documento e índices al costo de una operación?[cite:2]
  9. ¿En qué casos conviene usar lotes de escritura o transacciones?[cite:3]
  10. ¿Qué papel cumple la validación antes de escribir en Firestore?

Ejercicios prácticos

  1. Implementa una función crearAlumno() que use setDoc() con el UID como ID del documento y registre timestamps de creación y actualización.
  2. Implementa una función crearGrupo() usando addDoc() y justifica por qué elegiste Auto ID.
  3. Implementa una función obtenerConfiguracionSistema() que lea configuraciones/sistema y devuelva null si no existe.
  4. Implementa una función actualizarEstadoCurso() usando updateDoc() para cambiar solo el campo estado y updatedAt.
  5. Implementa una función inicializarPreferenciasUsuario() usando setDoc(..., { merge: true }) y explica por qué merge es más adecuado.
  6. Diseña una operación con writeBatch() que cree un curso y su configuración inicial en una sola confirmación.
  7. Diseña una transacción que incremente un contador de entregas sobre una tarea sin perder consistencia ante concurrencia.
  8. Revisa uno de los ejemplos del capítulo y explica qué costo operativo podría crecer si el documento se vuelve demasiado grande.[cite:2]

Bibliografía y referencias oficiales