Índice de contenido
- SQL vs. NoSQL
- Ventajas y Desventajas
- Alto rendimiento: FastAPI + MongoDB
- Flexibilidad vs. Consistencia
- El problema de la consistencia
- Conceptos Clave y Equivalencias
- Instalación en Windows
- Instalación en macOS (usando Homebrew)
- Requisitos previos
- macOS compatible
- Instalación de Homebrew
- Software necesario para instalar instalar MongoDB
- Instalar las Command line tools for xcode
- Instalar Homebrew
- ¿Qué es un tap de Homebrew?
- Comando para agregar el tap
- Elegir la versión de MongoDB
- Instalar el Tap de Homebrew de MongoDB
- Este es un Tap (paquete) de Homebrew personalizado para el software oficial de MongoDB.
- Interfaz Gráfica: MongoDB Compass
- Comprobación de la instalación de MongoDB
- Iniciar el proceso de MongoDB
- Iniciar el servicio
- Detener o reiniciar MongoDB
- Errores comunes y cómo resolverlos
- Error de conexión en localhost
- Problemas de versiones incompatibles
- Motor: El Driver Asíncrono para MongoDB
- Instalación de Dependencias
- Primera conexión a MongoDB con Motor
- Configuración del Cliente
- Limpieza de SQLAlchemy en FastAPI
- Ciclo de Vida con Lifespan en FastAPI
- Obtener el cliente de MongoDB
- Implementación del Router y Esquemas
- Del ORM al Driver Nativo
- Operaciones CRUD Paso a Paso
- 1. Crear Tarea (POST)
- _id y el ObjectId
- 2. Leer Todas las Tareas (GET)
- 3. Leer una Tarea específica (GET por ID)
- 4. Actualizar y Eliminar (PUT / DELETE)
- Verificación en Mongo Compass
- Ejemplo del Esquema Relacional en MongoDB
- Agregando y Removiendo Etiquetas
- 1. Añadir Etiquetas (Operador $addToSet)
- $addToSet + $each
- 2. Remover Etiquetas (Operador $pull)
- Flexibilidad del Esquema: ¿Dónde está la tabla de etiquetas?
- Cómo se almacenan las etiquetas en MongoDB
- Relaciones
- Esquemas Normalizados vs. Desnormalizados
- 1. Esquema Desnormalizado (Incrustado)
- 2. Esquema Normalizado (Referenciado)
- Cuándo usar cada uno? (1:1, 1:N y N:N)
- Operaciones Masivas: update_one vs update_many
- Conclusión y Práctica
Lo siguiente que vamos a hacer es empezar a configurar nuestro proyecto para poder utilizar MongoDB en cual le viene excelente la implementación que hicimos antes del CRUD automático con FastAPI. Vamos a utilizar la misma estructura del proyecto de base de datos que hicimos en el apartado anterior.
Vamos a conocer como podemos instalar MongoDB si estamos en MacOS; para esto vamos a partir de que tienes instalado Homebrew que es simplemente un gestor de paquetes para MacOS y Linux.
Realizar algunas prácticas para emplear una base de datos NoSQL, específicamente MongoDB, en FastAPI. Antes de comenzar, compararemos MongoDB con las bases de datos tradicionales de tipo relacional (SQL).
SQL vs. NoSQL
- Bases de Datos Relacionales (SQL): Como hemos visto hasta ahora, son estructuradas. Funcionan como tablas de Excel vinculadas entre sí. Tienen un esquema fijo; por ejemplo, si tienes una tabla de "tareas" con ID y Nombre, y luego quieres guardar una Descripción, debes modificar obligatoriamente el esquema de la base de datos.
- Bases de Datos NoSQL (MongoDB): Son bases de datos no estructuradas (o semi-estructuradas). Los datos se almacenan de manera flexible, generalmente en formatos similares a JSON. Esto permite cambiar la estructura sin previo aviso. Por ejemplo, podemos inyectar una "categoría" directamente dentro del esquema de una "tarea".
Ventajas y Desventajas
Ventajas:
- Flexibilidad: Ideal para prototipos rápidos y esquemas cambiantes.
- Escalabilidad masiva: Diseñadas para manejar volúmenes de datos gigantescos.
- Rapidez: Suelen ser más eficientes para operaciones de lectura/escritura simples.
Desventajas:
- Menor consistencia: Al no tener un esquema fijo, pueden volverse un lío si no se gestionan bien.
- Consultas complejas: Es más difícil realizar uniones (joins) o consultas muy intrincadas.
- Madurez: Aunque son populares, el ecosistema SQL tiene décadas de mayor soporte y estabilidad.
Alto rendimiento: FastAPI + MongoDB
El enfoque principal es ayudarte a romper con el esquema mental del SQL relacional para entrar en el mundo NoSQL.
- FastAPI es un framework reconocido por su altísimo rendimiento y velocidad.
- MongoDB es una base de datos diseñada precisamente para el alto rendimiento y la escalabilidad.
Esta combinación es ideal para proyectos de alta demanda donde la velocidad de respuesta es crítica.
Flexibilidad vs. Consistencia
Uno de los puntos que más repetiré es la menor consistencia de datos en favor de una mayor flexibilidad. En MongoDB, todo es un JSON (técnicamente BSON), lo que nos da una libertad total, pero también riesgos.
El problema de la consistencia
Imagina que tenemos una tarea con un campo categoría_id. Debido a la flexibilidad de MongoDB, podrías encontrarte con:
- Una tarea que tiene la categoría correctamente definida.
- Otra tarea que, por un error en el CRUD, no tiene el campo de categoría.
- Una tarea con etiquetas incrustadas directamente, sin una tabla relacional externa.
Si no administras bien tu lógica desde el código, al intentar consultar la categoría de un registro que no la tiene, tu aplicación podría lanzar un error 500. En MongoDB, la responsabilidad de mantener la integridad de los datos recae mucho más en el desarrollador y en cómo programa su CRUD.
Conceptos Clave y Equivalencias
Antes de comenzar con nuestro pequeño proyecto, es fundamental que hablemos el mismo idioma. Si vienes del mundo SQL, aquí tienes las equivalencias básicas:
Concepto SQL Equivalente en MongoDB
- Tabla Colección
- Registro/Fila Documento
- Columna Campo
Instalación en Windows
La instalación en Windows es muy sencilla:
- Busca en Google MongoDB Community Server.
- Descarga el instalador y sigue los pasos típicos (Next, Next, Finish).
- Configuración de variables de entorno: Es probable que debas agregar la ruta de instalación (usualmente la carpeta bin) a las variables de entorno del sistema.
- Tip: Haz clic derecho sobre "Mi Equipo" -> Propiedades -> Configuración avanzada -> Variables de entorno -> Path -> Agregar la ruta de la carpeta bin.
- Reinicia el equipo y ya podrás utilizar el comando mongosh.
Instalación en macOS (usando Homebrew)
Instalar MongoDB en macOS puede parecer complicado la primera vez, pero usando Homebrew el proceso es mucho más sencillo y limpio. En esta guía te explico paso a paso cómo instalar MongoDB en MacOS con Homebrew, cómo iniciarlo correctamente y cómo empezar a trabajar con la base de datos realizando operaciones CRUD básicas.
Este flujo es el que utilizo siempre que configuro un entorno de desarrollo nuevo en Mac, y evita la mayoría de errores típicos que suelen aparecer al arrancar MongoDB por primera vez.
Ya con nuestro gestor de paquetes, nada más fácil, lo primero que tenemos que hacer es agregar el repositorio de MongoDB a nuestro gestor de paquetes.
Requisitos previos
Antes de instalar MongoDB, es importante asegurarnos de que el sistema tiene todo lo necesario.
macOS compatible
MongoDB funciona correctamente en las versiones modernas de macOS (Catalina en adelante). Si usas una versión muy antigua, es recomendable actualizar el sistema o instalar una versión compatible de MongoDB.
Instalación de Homebrew
macOS no incluye Homebrew por defecto, y es una de las herramientas más importantes para desarrollo en Mac. Homebrew es un gestor de paquetes que permite instalar software desde la terminal de forma sencilla.
Para instalarlo, sigue las instrucciones oficiales desde su web:
Software necesario para instalar instalar MongoDB
Instalar las Command line tools for xcode
Seguramente cuando vayas a ejecutar el comando de Brew para instalar el paquete, te pedirá que instales las command line tools for xcode, acepta y descarga e instala estas herramientas.
Es muy probable que, al ejecutar cualquier comando de brew, macOS te pida instalar las Command Line Tools for Xcode.
Acepta el mensaje y deja que se instalen, ya que son necesarias para compilar y ejecutar muchas dependencias.
Instalar Homebrew
¿Qué es un tap de Homebrew?
Un tap es simplemente un repositorio adicional que Homebrew usa para encontrar paquetes. MongoDB mantiene su tap oficial, lo cual es importante para evitar instalaciones no soportadas.
Comando para agregar el tap
Ahora si, vamos a instalar Homebrew, MacOS no incluye el paquete de preparación Homebrew por defecto; por lo tanto, tienes que instalarlo como indica en la página oficial. https://brew.sh/#install
Homebrew instala las cosas que necesitas para tu MacOS desde una terminal fácilmente.
$ brew tap mongodb/brewEste paso es clave; muchos errores vienen de intentar instalar MongoDB sin usar el tap oficial.
Elegir la versión de MongoDB
MongoDB publica varias versiones. En este caso vamos a instalar una versión estable específica, que es la que mejor resultado me ha dado en macOS:
Instalar el Tap de Homebrew de MongoDB
Emita lo siguiente desde el terminal para tocar el grifo oficial de MongoDB Homebrew: https://github.com/mongodb/homebrew-brew
Este es un Tap (paquete) de Homebrew personalizado para el software oficial de MongoDB.
$ brew tap mongodb/brewLuego de esto, instalamos la última versión a la fecha, que al momento de decir estas palabras sería:
$ brew install mongodb-community@8.2O puedes instalar una versión especifica
$ brew install mongodb-community@8.0
$ brew install mongodb-community@7.0Instalar una versión concreta evita incompatibilidades con librerías o con el sistema operativo, algo que ya me ha ahorrado más de un dolor de cabeza.
Interfaz Gráfica: MongoDB Compass
Para trabajar de una manera más agradable y no depender solo de la terminal, instalaremos MongoDB Compass, la herramienta oficial de interfaz gráfica.
- En Windows: Se puede seleccionar durante la instalación del servidor o descargar por separado desde la web oficial.
- En macOS (vía Homebrew):
$ brew install --cask mongodb-compass- (El parámetro --cask indica que estamos instalando una aplicación con interfaz gráfica).
Una vez instalado, lo encontrarás en tu carpeta de Aplicaciones. Ábrelo, conéctate al servidor local y ya estarás listo para gestionar tus colecciones de datos.
También puedes instalarlo mediante el instalador en MacOS Windows:
https://www.mongodb.com/try/download/compass
Comprobación de la instalación de MongoDB
Una vez finalizado el proceso, MongoDB ya estará instalado en tu equipo, pero aún no estará en ejecución.
Iniciar el proceso de MongoDB
Ya con esto tenemos MongoDB en nuestro equipo; lo siguiente que vamos a hacer es iniciar el proceso, ya que si ejecutamos en nuestra terminal:
$ brew services start mongodb-communityEste comando es fundamental. Si no inicias el servicio y ejecutas mongo directamente, obtendrás un error de conexión.
Ya que si no lo inicias y escribes, mongo en la terminal, verás un error como el siguiente:
MongoDB shell version v8.0.2
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Error: couldn't connect to server 127.0.0.1:27017, connection attempt failed: SocketException: Error connecting to 127.0.0.1:27017 :: caused by :: Connection refused :
connect@src/mongo/shell/mongo.js:372:17Esto ocurre porque MongoDB no está escuchando en el puerto 27017.
Una vez iniciado correctamente, el comando:
$ mongoTe permitirá acceder al shell sin problemas, o ver su versión instalada:
$ mongod --versionIniciar el servicio
Al igual que ocurre con servicio como MySQL, para poder emplearlo, debemos de iniciar el servicio; ya que, si intentamos iniciar el asistende te Mongo sin iniciar:
$ mongoshVeremos un error como el siguiente:
Current Mongosh Log ID: 699c28e47c1b4855cf41cae5
Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.7.0
MongoNetworkError: connect ECONNREFUSED 127.0.0.1:27017Que dice que intenta conectarse pero el servidor de MongoDB NO respondió; iniciamos el servicio:
brew services start mongodb-community@8.2Y ahora, si ejecutas el:
$ mongoshDebería de saludarte con un:
test>Detener o reiniciar MongoDB
Algunos comandos útiles que suelo usar:
$ brew services stop mongodb-community
$ brew services restart mongodb-communityErrores comunes y cómo resolverlos
Error de conexión en localhost
Casi siempre se debe a que el servicio no está iniciado. Verifica con:
$ brew services listProblemas de versiones incompatibles
Si cambiaste de versión de macOS o actualizaste MongoDB, puede ser necesario reinstalar la versión correcta o limpiar servicios antiguos.
Motor: El Driver Asíncrono para MongoDB
Para trabajar con MongoDB en Python existen muchos conectores, pero nosotros vamos a emplear Motor.
Motor es un driver diseñado específicamente para trabajar de forma asíncrona. Como comentamos anteriormente, MongoDB está pensado para grandes volúmenes de datos y alta concurrencia. Si sumamos esto al esquema asíncrono de FastAPI, obtenemos una herramienta fundamental para que las peticiones se manejen de manera mucho más eficiente. Tiene todo el sentido del mundo utilizar un servicio asíncrono para aplicaciones de alto consumo, que es precisamente el propósito de MongoDB.
Instalación de Dependencias
Para instalar la herramienta, el proceso es el habitual mediante pip. Recuerda tener tu ambiente virtual activo antes de ejecutar el comando.
Ten en cuenta que, al avanzar, iremos borrando las dependencias que ya no necesitemos, como todo lo relacionado con SQLite y SQLAlchemy, ya que MongoDB no requiere de estas librerías.
Para instalar Motor, ejecuta:
$ pip install motorPrimera conexión a MongoDB con Motor
Vamos a realizar nuestra primera conexión a MongoDB. Creamos un archivo db_connection.py. En él, importamos el cliente de Motor para gestionar la conexión asíncrona:
db_connection.py
import logging
from motor.motor_asyncio import AsyncIOMotorClient
logger = logging.getLogger("uvicorn.error")
mongo_client = AsyncIOMotorClient(
"mongodb://localhost:27017"
)
async def ping_mongo_db_server():
try:
await mongo_client.admin.command("ping")
logger.info("Connected to MongoDB")
except Exception as e:
logger.error(
f"Error connecting to MongoDB: {e}"
)
raise eConfiguración del Cliente
Antes de empezar, asegúrate de que el servicio de MongoDB esté activo (como vimos en el primer video). Si al escribir mongosh en tu terminal recibes el mensaje de bienvenida, todo está en orden; de lo contrario, debes iniciarlo.
Definimos la URL de conexión, que por defecto utiliza el puerto 27017 (similar a como MySQL utiliza el 3306).
Creamos una función para realizar un "ping" al servidor. Si el intento es exitoso, mostraremos un mensaje indicando que estamos conectados; de lo contrario, lanzaremos una excepción para advertir que hay problemas de conexión.
Limpieza de SQLAlchemy en FastAPI
En el archivo principal de tu API, debes comentar o eliminar todo lo relacionado con SQLAlchemy y bases de datos relacionales, ya que ahora utilizaremos MongoDB.
- Importaciones: Comenta las líneas de SQL Alchemy y las rutas que dependan de él.
- Dependencias: Puedes eliminar la función que gestionaba la sesión de la base de datos relacional.
- Modelos: Ya no necesitaremos crear tablas al inicio, puesto que MongoDB no requiere un esquema fijo predefinido.
Ciclo de Vida con Lifespan en FastAPI
Para gestionar la conexión de forma eficiente, utilizaremos el manejador de eventos Lifespan. Si no lo conocías, es una forma sencilla de controlar los ciclos de vida de la aplicación.
- Antes del yield: Todo lo que coloques aquí se ejecutará antes de que la aplicación empiece a recibir peticiones. Es el lugar ideal para iniciar el cliente de MongoDB.
- Después del yield: Aquí colocaremos la lógica para cerrar la conexión o limpiar recursos cuando la aplicación se detenga.
Finalmente, configuramos este lifespan al crear la instancia de FastAPI:
api.py
from fastapi import FastAPI, Depends, APIRouter, Query, Path
from contextlib import asynccontextmanager
from db_connection import ping_mongo_db_server
@asynccontextmanager
async def lifespan(app: FastAPI):
await ping_mongo_db_server()
yield
app = FastAPI(lifespan=lifespan)Al iniciar el servidor, deberías ver en la consola el mensaje: "Conectado a MongoDB". Esto confirma que la operación fue exitosa y ya estamos listos para empezar a trabajar con colecciones y documentos.
Obtener el cliente de MongoDB
Ahora, vamos a implementar un servicio que se encarga de gestionar la conexión. Este servicio devuelve la instancia de la base de datos ya lista para usar:
mongo_db.py
from db_connection import mongo_client
# Define la base de datos que contendrá todas las colecciones de nuestra aplicación.
# La librería motor la creará automáticamente si no existe.
database = mongo_client.task_manager
def get_mongo_database():
"""Devuelve la base de datos que se usará como dependencia."""
return databaseImplementación del Router y Esquemas
En el archivo de la API, configuramos el ruteo bajo el tag Mongo Tasks:
api.py
from mongo_task import mongo_task_router
***
app.include_router(mongo_task_router, prefix="/mongo/tasks", tags=["Mongo Tasks"])Notarás que, aunque la estructura es similar a la que usamos con SQLAlchemy, existen diferencias clave en los métodos y en cómo manejamos los datos.
Del ORM al Driver Nativo
Anteriormente utilizábamos un ORM para bases de datos relacionales. En MongoDB, al ser una base de datos documental, la nomenclatura cambia:
- En lugar de métodos de SQL tradicional, usamos funciones como insert_one, find, update_one o delete_one.
- Estructura de datos: MongoDB trabaja de forma nativa con estructuras similares a JSON. En el caso de Python, esto se traduce en el uso constante de diccionarios
Operaciones CRUD Paso a Paso
Comencemos con las importaciones iniciales:
from fastapi import APIRouter, Body, Depends, HTTPException, status, Path
from pymongo.database import Database
from bson import ObjectId
from mongo_db import get_mongo_database
from schemes import TaskWrite
mongo_task_router = APIRouter()1. Crear Tarea (POST)
Convertimos el modelo a diccionario e insertamos el registro. Es una operación asíncrona que nos devuelve el ID generado.
mongo_task.py
# CREATE
@mongo_task_router.post("/", status_code=status.HTTP_201_CREATED, summary="Crear una nueva tarea")
async def add_task(
task: TaskWrite = Body(...),
db: Database = Depends(get_mongo_database),
):
"""
Crea una nueva tarea en la base de datos.
"""
# task_dict = task.dict()
task_dict = task.model_dump()
insert_result = await db.tasks.insert_one(task_dict)
return {
"message": "Tarea añadida correctamente",
"id": str(insert_result.inserted_id),
}_id y el ObjectId
Al insertar tu primera tarea, notarás que el identificador no es un número incremental (1, 2, 3...), sino una cadena hexadecimal extraña llamada ObjectId.
¿Por qué no es un número secuencial?
Las bases de datos relacionales suelen ser centralizadas, lo que facilita llevar un conteo exacto. Sin embargo, MongoDB está diseñado para ser descentralizado.
Si tuviéramos varios servidores de MongoDB funcionando en paralelo, dos servidores podrían intentar asignar el ID "5" al mismo tiempo, generando un conflicto. El ObjectId soluciona esto combinando varios factores:
- Timestamp: La hora exacta de creación (esto garantiza que sea único en el tiempo).
- Identificador de proceso y contador: Datos aleatorios que aseguran la unicidad incluso si dos registros se crean en el mismo milisegundo.
2. Leer Todas las Tareas (GET)
Usamos el método find(). Es importante convertir el cursor que devuelve Mongo a una lista mediante to_list() para que FastAPI pueda retornarlo como un JSON.
mongo_task.py
# READ ALL
@mongo_task_router.get("/", summary="Obtener todas las tareas")
async def get_all_tasks(db: Database = Depends(get_mongo_database)):
"""
Obtiene todas las tareas de la colección 'tasks'.
"""
tasks_cursor = db.tasks.find()
return await tasks_cursor.to_list(length=None)3. Leer una Tarea específica (GET por ID)
Aquí aplicamos una validación doble:
- Validación de formato: Verificamos si el string recibido es un ObjectId válido. Si no lo es, devolvemos un error 400 de inmediato para ahorrar recursos.
- Búsqueda: Si el formato es correcto pero no existe el registro, devolvemos un 404.
mongo_task.py
# READ ONE
@mongo_task_router.get("/{task_id}", summary="Obtener una tarea")
async def get_task(
task_id: str = Path(..., description="El ID de la tarea a obtener"),
db: Database = Depends(get_mongo_database),
):
"""
Obtiene una única tarea por su ID.
"""
if not ObjectId.is_valid(task_id):
raise HTTPException(status_code=400, detail=f"ObjectId inválido: {task_id}")
task = await db.tasks.find_one({"_id": ObjectId(task_id)})
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tarea con id {task_id} no encontrada"
)
return task4. Actualizar y Eliminar (PUT / DELETE)
En la actualización, enviamos solo los campos que queremos modificar. Para la eliminación, simplemente buscamos por el _id y ejecutamos delete_one. Si el conteo de documentos afectados es cero, informamos que no se realizó ninguna acción.
mongo_task.py
# UPDATE
@mongo_task_router.put("/{task_id}", summary="Actualizar una tarea")
async def update_task(
task_id: str = Path(..., description="El ID de la tarea a actualizar"),
task: TaskWrite = Body(...),
db: Database = Depends(get_mongo_database),
):
"""
Actualiza los campos de una tarea.
"""
if not ObjectId.is_valid(task_id):
raise HTTPException(status_code=400, detail=f"ObjectId inválido: {task_id}")
# update_data = task.dict(exclude_unset=True)
update_data = task.model_dump(exclude_none=True)
if not update_data:
raise HTTPException(status_code=400, detail="No se proporcionaron datos para actualizar")
result = await db.tasks.update_one({"_id": ObjectId(task_id)}, {"$set": update_data})
if result.matched_count == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Tarea con id {task_id} no encontrada")
if result.modified_count == 1:
updated_task = await db.tasks.find_one({"_id": ObjectId(task_id)})
return updated_task
return {"message": "Los datos de la tarea eran los mismos, no se realizó ninguna actualización."}
# DELETE
@mongo_task_router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Eliminar una tarea")
async def delete_task(
task_id: str = Path(..., description="El ID de la tarea a eliminar"),
db: Database = Depends(get_mongo_database),
):
"""
Elimina una tarea de la base de datos.
"""
if not ObjectId.is_valid(task_id):
raise HTTPException(status_code=400, detail=f"ObjectId inválido: {task_id}")
result = await db.tasks.delete_one({"_id": ObjectId(task_id)})
if result.deleted_count == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Tarea con id {task_id} no encontrada")
returnVerificación en Mongo Compass
Una vez ejecutadas las pruebas desde la documentación interactiva de FastAPI (Swagger UI), puedes refrescar Mongo Compass. Verás cómo los documentos se almacenan con su estructura flexible de tipo JSON.
Este flujo demuestra lo transparente que puede ser cambiar de un esquema relacional a uno NoSQL si se tiene una buena arquitectura.
Ejemplo del Esquema Relacional en MongoDB
¿Qué es lo que queremos hacer? Actualmente tenemos la entidad Task (Tarea), pero ahora le quiero agregar una lista de etiquetas (tags). En este caso es una lista de strings, aunque podría ser cualquier otra cosa. Ya aquí comienza lo "raro": si esto fuera un esquema relacional puro, la relación no sería tan directa. Usualmente, definiríamos una propiedad igual a un ID que se encuentra en otra entidad. Sin embargo, aquí no lo hice así porque me interesa que las etiquetas sean simplemente texto incrustado.
En cuanto al modelo de datos (Pydantic), simplemente agregamos el campo tags, que es una lista. No tenemos una tabla adicional para etiquetas, y aquí es donde quiero que reflexiones:
schemes.py
class Task(BaseModel):
name: str
description: Optional[str] = Field("No description",min_length=5)
status: StatusType
tags: List[str] = []
***
class TagsUpdate(BaseModel):
tags: List[str] ¿Dónde vamos a guardar esas etiquetas si no existe una tabla independiente como en un esquema relacional?
En un modelo relacional, obligatoriamente tendríamos una tabla para tareas y otra para etiquetas, probablemente con una tabla intermedia.
En MongoDB no. Aquí rompemos con ese esquema tradicional.
MongoDB trabaja con documentos JSON. Y un JSON puede contener un array. Ese array es precisamente el campo tags.
Agregando y Removiendo Etiquetas
En el archivo mongo_task.py es donde están los cambios principales. La parte inicial (obtener datos e insertar) se mantiene igual. Para facilitar la lectura, me enfocaré en la parte de la manipulación de etiquetas, que es un poco más abstracta.
mongo_task.py
# ADD TAGS
@mongo_task_router.put("/{task_id}/tags/add", summary="Añadir tags a una tarea")
async def add_tags_to_task(
task_id: str = Path(..., description="El ID de la tarea a actualizar"),
tags_update: TagsUpdate = Body(..., example={"tags": ["new_tag_1", "new_tag_2"]}),
db: Database = Depends(get_mongo_database),
):
"""
Añade uno o más tags a una tarea existente.
Usa $addToSet para evitar duplicados en el array de tags.
"""
if not ObjectId.is_valid(task_id):
raise HTTPException(status_code=400, detail=f"ObjectId inválido: {task_id}")
result = await db.tasks.update_one(
{"_id": ObjectId(task_id)},
{"$addToSet": {"tags": {"$each": tags_update.tags}}}
)
if result.matched_count == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Tarea con id {task_id} no encontrada")
updated_task = await db.tasks.find_one({"_id": ObjectId(task_id)})
return updated_task
# REMOVE TAGS
@mongo_task_router.put("/{task_id}/tags/remove", summary="Eliminar tags de una tarea")
async def remove_tags_from_task(
task_id: str = Path(..., description="El ID de la tarea a actualizar"),
tags_update: TagsUpdate = Body(..., example={"tags": ["tag_to_remove_1", "tag_to_remove_2"]}),
db: Database = Depends(get_mongo_database),
):
"""
Elimina uno o más tags de una tarea existente.
Usa $pull para remover las instancias de los tags especificados.
"""
if not ObjectId.is_valid(task_id):
raise HTTPException(status_code=400, detail=f"ObjectId inválido: {task_id}")
result = await db.tasks.update_one(
{"_id": ObjectId(task_id)},
{"$pull": {"tags": {"$in": tags_update.tags}}}
)
if result.matched_count == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Tarea con id {task_id} no encontrada")
updated_task = await db.tasks.find_one({"_id": ObjectId(task_id)})
return updated_taskLa parte inicial (obtener datos e insertar) se mantiene igual. Para facilitar la lectura, me enfocaré en la parte de la manipulación de etiquetas, que es un poco más abstracta.
1. Añadir Etiquetas (Operador $addToSet)
He agregado métodos para manipular las etiquetas de una tarea mediante su task_id. Fíjate que recibimos un array de datos de golpe. En un esquema relacional, si quisieras agregar 10 etiquetas, tendrías que hacer 10 inserciones o una operación compleja entre tablas. Aquí no; todo se hace en una sola operación.
Para actualizar, usamos update_one con un operador llamado $addToSet.
¿Por qué $addToSet? Porque MongoDB trabaja con formato JSON, y un JSON puede tener un JSONArray incrustado. Este operador nos permite añadir elementos a ese array asegurándonos de que no se repitan, y lo hace de forma atómica.
Para no iterar manualmente con un for en nuestro código (lo cual sería ineficiente), usamos el modificador $each. Esto permite que MongoDB itere internamente y agregue todos los valores de una sola vez.
$addToSet + $each
Utilizamos el operador $addToSet junto con el modificador $each.
- $addToSet agrega valores sin duplicarlos.
- $each permite iterar internamente sobre el array recibido.
Esto evita tener que:
- Hacer 10 peticiones a la API.
- Iterar manualmente los valores.
- Ejecutar múltiples operaciones en la base de datos.
2. Remover Etiquetas (Operador $pull)
Para eliminar etiquetas, la lógica es similar pero utilizamos el operador $pull.
- ¿Cómo funciona? Recibimos un array con los elementos a remover.
- El operador $in: Se encarga de buscar cuáles de las etiquetas que enviamos existen realmente en el documento.
- El operador $pull: Las extrae del listado.
Internamente usamos $pull junto con $in, que permite comparar múltiples valores.
Es muy flexible: si envías una etiqueta que no existe en la tarea, simplemente la pasa de largo sin lanzar errores, como si fuera un condicional silencioso.
Y si revisamos la base de datos al hacer algunas operaciones:
tasks (coleción)
{
"_id": {
"$oid": "699c6024ba20f652d828f93c"
},
"name": "Task 1",
"description": "No description",
"status": "done",
"category_id": 1,
"user_id": 0,
"id": 0,
"tags": [
"Tag 1",
"Tag 3",
"Tag 4"
]
}
{
"_id": {
"$oid": "699d86e1701f772d79b49f03"
},
"name": "string",
"description": "No description",
"status": "done",
"tags": [
"Tag 2",
"Tag 3"
],
"id": "string"
}Flexibilidad del Esquema: ¿Dónde está la tabla de etiquetas?
Aquí es donde quiero que te preguntes: ¿Dónde están las etiquetas? En el mundo relacional, tendrías una tabla Tags y quizás una tabla pivote. Aquí no existen. Las etiquetas están incrustadas dentro del mismo JSON de la tarea.
Esto tiene ventajas y desventajas:
- Lo bueno: Cuando consultas una tarea, ya traes sus etiquetas "de un solo golpe" sin necesidad de hacer un JOIN. Es mucho más rápido para grandes cargas de datos.
- Lo malo: No hay un sistema de migraciones estricto. Como viste en el ejercicio, modifiqué la estructura agregando la columna tags y a MongoDB no le importó; simplemente empezó a guardar el nuevo campo en los documentos nuevos o actualizados.
Cómo se almacenan las etiquetas en MongoDB
Recordemos que MongoDB almacena documentos en formato JSON:
{
"id": 1,
"title": "Tarea 1",
"tags": ["tag1", "tag2"]
}Aquí las etiquetas están incrustadas dentro del mismo documento de la tarea. No existe una tabla aparte.
Eso tiene una gran ventaja: cuando consultamos la tarea, ya obtenemos todas sus etiquetas en una sola operación, sin necesidad de hacer JOIN.
Esto puede ser bueno o malo, dependiendo del caso de uso, pero en términos de rendimiento y simplicidad, es bastante eficiente.
Relaciones
Vamos a resumir rápidamente para afianzar lo más importante. En la clase anterior veíamos cómo manejar relaciones en MongoDB y, aunque no lo mencioné explícitamente, estamos trabajando con una relación de tipo Muchos a Muchos (N:N).
¿Por qué es Muchos a Muchos? Veámoslo en la práctica:
Tenemos una tarea con las etiquetas 1, 3 y 4, y otra tarea que no tiene nada. Si agregamos la "Tag 3" a esa segunda tarea, ahora ambas comparten la misma etiqueta.
Sé que te estarás preguntando: "¿Esto no es una chapuza? No hay una clave foránea ni índices relacionales". Aquí es donde debes abrir la mente: MongoDB no es una base de datos relacional. Se rompe el esquema de "tabla" para entender que todo es un JSON. MongoDB es, en esencia, un gestor que nos permite manipular esos JSON con gran flexibilidad. Aunque el vínculo sea solo un texto (string), si el valor "Etiqueta 3" es idéntico en ambos documentos, existe una relación funcional.
Esquemas Normalizados vs. Desnormalizados
En MongoDB podemos seguir dos caminos para estructurar los datos:
1. Esquema Desnormalizado (Incrustado)
Es el que estamos usando. Guardamos el valor directamente (el texto de la etiqueta) dentro de la tarea.
- Ventaja: No necesitas tablas pivote ni joins. Al traer la tarea, ya tienes toda la información "de un solo manguerazo".
- Vínculo: El valor mismo es el vínculo. Es ideal si los datos no cambian frecuentemente.
2. Esquema Normalizado (Referenciado)
Es el equivalente al esquema relacional. En lugar de guardar el texto "Tag 3", guardamos el ID (u ObjectId) que hace referencia a un documento en una colección aparte llamada tags.
- Estructura: Tendrías un array de IDs llamado tag_ids.
- Uso: Se recomienda cuando la entidad relacionada (la etiqueta o el usuario) sufre muchas actualizaciones. Si cambias el nombre de una etiqueta en su colección propia, el cambio se refleja en todos lados porque las tareas solo apuntan al ID.
Cuándo usar cada uno? (1:1, 1:N y N:N)
Todo depende de tu lógica de negocio y la frecuencia de actualización:
- Relación Uno a Muchos (1:N): Como las categorías. Si una tarea pertenece a una categoría, en lugar de un array, simplemente tendrías un campo category que puede ser el nombre o una referencia.
- Relación Uno a Uno (1:1): Ejemplo: Usuarios y Direcciones. Como una dirección suele ser única para un usuario, lo más lógico es que el esquema de la dirección viva embebido (dentro) del objeto usuario. No tiene sentido crear una colección aparte para algo que no se va a compartir.
Operaciones Masivas: update_one vs update_many
Si usas el esquema desnormalizado y necesitas renombrar una etiqueta en todas las tareas, no puedes usar update_one. Para eso existe update_many.
MongoDB ofrece estos métodos precisamente por su naturaleza flexible. Si tienes 1,000 tareas con la etiqueta "Antiguo" y quieres que ahora digan "Nuevo", lanzas un update_many que busque ese valor y lo reemplace en toda la colección de golpe.
Conclusión y Práctica
La mejor forma de entender esto es rompiendo el esquema mental de las tablas de Excel. Te dejo como tarea investigar o pedirle a tu asistente que genere el código para un update_many siguiendo nuestro esquema. Tienes el código fuente en el repositorio para comparar.
Prueba a crear un modelo donde las direcciones sean un objeto embebido o intenta simular una relación 1:N con categorías. Solo practicando entenderás cuándo te conviene la velocidad de lo desnormalizado o la integridad de lo normalizado.
Código fuente:
https://github.com/libredesarrollo/fastapi-book-course-mongodb