TechLead
Lección 3 de 8
7 min de lectura
Firebase

Cloud Firestore

Aprende a usar Cloud Firestore, la base de datos NoSQL flexible y escalable de Firebase

¿Qué es Cloud Firestore?

Cloud Firestore es la base de datos NoSQL en la nube de Firebase, flexible y escalable, que almacena y sincroniza datos para desarrollo del lado del cliente y del servidor. Mantiene los datos sincronizados en tiempo real mediante listeners y ofrece soporte offline para que tu app funcione incluso sin conexión.

A diferencia de las bases de datos relacionales tradicionales, Firestore organiza los datos en documentos y colecciones, lo que facilita su uso y permite estructuras de datos flexibles.

📚 Modelo de datos de Firestore

  • 📄 Documentos: Registros ligeros con campos y valores (como objetos JSON).
  • 📁 Colecciones: Contenedores de documentos. Una colección solo contiene documentos.
  • 🔗 Subcolecciones: Colecciones dentro de documentos, para estructuras anidadas.
  • 🔍 Referencias: Punteros a documentos o colecciones en tu base de datos.

Configurar Firestore

Inicializa Firestore en tu aplicación:

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

const firebaseConfig = {
  // Your config here
};

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

export { db };

Agregar documentos

Hay varias formas de añadir documentos en Firestore:

Agregar con ID autogenerado

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

async function addUser(userData) {
  try {
    const docRef = await addDoc(collection(db, 'users'), {
      name: userData.name,
      email: userData.email,
      createdAt: new Date(),
      role: 'user'
    });
    console.log('Document written with ID:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('Error adding document:', error);
    throw error;
  }
}

Crear con ID personalizado

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

async function createUserProfile(userId, profileData) {
  try {
    await setDoc(doc(db, 'users', userId), {
      name: profileData.name,
      email: profileData.email,
      bio: profileData.bio,
      updatedAt: new Date()
    });
    console.log('Profile created for user:', userId);
  } catch (error) {
    console.error('Error creating profile:', error);
  }
}

// Merge with existing data instead of overwriting
await setDoc(doc(db, 'users', userId), {
  lastLogin: new Date()
}, { merge: true });

Leer documentos

Obtén documentos individuales o colecciones completas:

Obtener un documento

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

async function getUser(userId) {
  const docRef = doc(db, 'users', userId);
  const docSnap = await getDoc(docRef);

  if (docSnap.exists()) {
    console.log('User data:', docSnap.data());
    return { id: docSnap.id, ...docSnap.data() };
  } else {
    console.log('No such document!');
    return null;
  }
}

Obtener todos los documentos de una colección

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

async function getAllUsers() {
  const querySnapshot = await getDocs(collection(db, 'users'));
  const users = [];

  querySnapshot.forEach((doc) => {
    users.push({ id: doc.id, ...doc.data() });
  });

  return users;
}

Consultar documentos

Filtra y ordena documentos con consultas:

import {
  collection,
  query,
  where,
  orderBy,
  limit,
  getDocs
} from 'firebase/firestore';

// Simple query
async function getActiveUsers() {
  const q = query(
    collection(db, 'users'),
    where('status', '==', 'active')
  );

  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Complex query with multiple conditions
async function searchUsers(role, minAge) {
  const q = query(
    collection(db, 'users'),
    where('role', '==', role),
    where('age', '>=', minAge),
    orderBy('age', 'asc'),
    limit(10)
  );

  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Query operators: ==, !=, <, <=, >, >=, array-contains,
// array-contains-any, in, not-in

Actualizaciones en tiempo real

Escucha cambios de datos en vivo:

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

// Listen to a single document
function subscribeToUser(userId, callback) {
  const unsubscribe = onSnapshot(doc(db, 'users', userId), (doc) => {
    if (doc.exists()) {
      callback({ id: doc.id, ...doc.data() });
    }
  });

  // Return unsubscribe function to stop listening
  return unsubscribe;
}

// Listen to a collection
function subscribeToUsers(callback) {
  const q = query(collection(db, 'users'), where('status', '==', 'active'));

  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const users = [];
    querySnapshot.forEach((doc) => {
      users.push({ id: doc.id, ...doc.data() });
    });
    callback(users);
  });

  return unsubscribe;
}

// Usage in React
useEffect(() => {
  const unsubscribe = subscribeToUsers((users) => {
    setUsers(users);
  });

  return () => unsubscribe(); // Cleanup on unmount
}, []);

Actualizar documentos

Modifica documentos existentes:

import { doc, updateDoc, arrayUnion, arrayRemove, increment } from 'firebase/firestore';

// Simple update
async function updateUser(userId, data) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    name: data.name,
    updatedAt: new Date()
  });
}

// Update specific fields
async function updateUserEmail(userId, newEmail) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    email: newEmail
  });
}

// Add to an array
async function addTag(userId, tag) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    tags: arrayUnion(tag)
  });
}

// Remove from an array
async function removeTag(userId, tag) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    tags: arrayRemove(tag)
  });
}

// Increment a number
async function incrementScore(userId, points) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    score: increment(points)
  });
}

Eliminar documentos

Elimina documentos y campos:

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

// Delete a document
async function deleteUser(userId) {
  await deleteDoc(doc(db, 'users', userId));
  console.log('User deleted');
}

// Delete a specific field
async function removeUserBio(userId) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    bio: deleteField()
  });
}

Subcolecciones

Organiza datos con colecciones anidadas:

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

// Add a document to a subcollection
async function addPost(userId, postData) {
  const postsRef = collection(db, 'users', userId, 'posts');
  const docRef = await addDoc(postsRef, {
    title: postData.title,
    content: postData.content,
    createdAt: new Date()
  });
  return docRef.id;
}

// Get all documents from a subcollection
async function getUserPosts(userId) {
  const postsRef = collection(db, 'users', userId, 'posts');
  const querySnapshot = await getDocs(postsRef);

  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
}

// Reference a specific document in a subcollection
const postRef = doc(db, 'users', userId, 'posts', postId);

Escrituras en lote y transacciones

Ejecuta múltiples operaciones de forma atómica:

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

// Batch writes - up to 500 operations
async function batchUpdate() {
  const batch = writeBatch(db);

  const user1Ref = doc(db, 'users', 'user1');
  const user2Ref = doc(db, 'users', 'user2');

  batch.update(user1Ref, { status: 'inactive' });
  batch.update(user2Ref, { status: 'inactive' });
  batch.delete(doc(db, 'users', 'user3'));

  await batch.commit();
  console.log('Batch completed');
}

// Transactions - for read-then-write operations
async function transferPoints(fromId, toId, points) {
  await runTransaction(db, async (transaction) => {
    const fromRef = doc(db, 'users', fromId);
    const toRef = doc(db, 'users', toId);

    const fromDoc = await transaction.get(fromRef);
    const toDoc = await transaction.get(toRef);

    if (!fromDoc.exists() || !toDoc.exists()) {
      throw new Error('User not found');
    }

    const fromPoints = fromDoc.data().points;
    if (fromPoints < points) {
      throw new Error('Insufficient points');
    }

    transaction.update(fromRef, { points: fromPoints - points });
    transaction.update(toRef, { points: toDoc.data().points + points });
  });
}

Tipos de datos en Firestore

Firestore soporta varios tipos de datos:

import { Timestamp, GeoPoint, serverTimestamp } from 'firebase/firestore';

const document = {
  // Primitive types
  name: 'John Doe',           // string
  age: 30,                     // number
  isActive: true,              // boolean
  nullable: null,              // null

  // Complex types
  tags: ['developer', 'designer'],  // array
  address: {                         // map (nested object)
    city: 'San Francisco',
    country: 'USA'
  },

  // Special Firestore types
  createdAt: serverTimestamp(),           // Server timestamp
  birthday: Timestamp.fromDate(new Date('1990-01-15')),  // Timestamp
  location: new GeoPoint(37.7749, -122.4194),  // GeoPoint

  // Reference to another document
  teamRef: doc(db, 'teams', 'team123')
};

💡 Puntos clave

  • • Firestore organiza los datos en colecciones y documentos
  • • Usa addDoc para IDs autogenerados y setDoc para IDs personalizados
  • • Los listeners en tiempo real con onSnapshot mantienen datos sincronizados
  • • Consulta documentos con where, orderBy y limit
  • • Usa transacciones para operaciones de lectura y escritura
  • • Las subcolecciones ayudan a organizar datos jerárquicos

📚 Más recursos

Continuar Aprendiendo