Índice de contenido
- SQL vs. NoSQL
- Ventajas y Desventajas
- Alto rendimiento: FastAPI + MongoDB con Motor
- 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 MongoDB
- Instalar las Command Line Tools for Xcode
- Agregar el Tap oficial de MongoDB a Homebrew
- ¿Qué es un tap de Homebrew?
- Comando para agregar el tap
- Elegir la versión de MongoDB a instalar
- Interfaz Gráfica: MongoDB Compass
- Comprobación de la instalación de MongoDB
- Iniciar el proceso de MongoDB
- Iniciar el servicio con versión específica
- 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 en Python
- 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 como dependencia
- Implementación del Router y Esquemas
- Del ORM al Driver Nativo
- Operaciones CRUD Paso a Paso
- 1. Crear Tarea (POST)
- El _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 MongoDB Compass
- Relaciones en MongoDB: Esquema Incrustado con Etiquetas
- Agregando y Removiendo Etiquetas
- 1. Añadir Etiquetas: el Operador $addToSet
- $addToSet + $each: la combinación perfecta
- 2. Remover Etiquetas: el Operador $pull
- Flexibilidad del Esquema: ¿Dónde está la tabla de etiquetas?
- Cómo se almacenan las etiquetas en MongoDB
- Relaciones en MongoDB
- Esquemas Normalizados vs. Desnormalizados
- 1. Esquema Desnormalizado (Incrustado / Embedded)
- 2. Esquema Normalizado (Referenciado / Referenced)
- ¿Cuándo usar cada estrategia? (1:1, 1:N y N:N)
- Operaciones Masivas: update_one vs update_many
- Conclusión y Práctica
En este capítulo vamos a configurar nuestro proyecto para integrar MongoDB con FastAPI usando el driver Motor. Esta integración encaja de forma natural con la estructura del proyecto que construimos en el capítulo anterior sobre el CRUD automático con FastAPI, y aprovecharemos esa misma base para migrar hacia una base de datos NoSQL.
Antes de entrar en código, haremos un repaso práctico de qué es MongoDB, cómo se diferencia de las bases de datos relacionales y por qué tiene tanto sentido combinarlo con FastAPI. También veremos cómo instalar MongoDB en macOS usando Homebrew, el gestor de paquetes más popular para macOS y Linux.
SQL vs. NoSQL
- Bases de Datos Relacionales (SQL): Son estructuradas y funcionan como tablas de Excel interconectadas. Tienen un esquema rígido y predefinido; por ejemplo, si tienes una tabla de
tareascon los camposidynombre, y luego necesitas guardar unadescripcion, debes modificar obligatoriamente el esquema de la base de datos con una migración. - Bases de Datos NoSQL (MongoDB): Son bases de datos no estructuradas (o semi-estructuradas). Los datos se almacenan de forma flexible, generalmente en formatos similares a
JSON. Esto permite cambiar la estructura sin necesidad de migraciones previas. Por ejemplo, podemos inyectar unacategoriadirectamente dentro del documento de unatareasin tocar ningún esquema.
Ventajas y Desventajas
Ventajas:
- Flexibilidad: Ideal para prototipos rápidos y esquemas que evolucionan constantemente.
- Escalabilidad masiva: Diseñadas para manejar volúmenes de datos gigantescos de forma horizontal.
- Rapidez: Suelen ser más eficientes para operaciones de lectura/escritura simples, especialmente cuando no se requieren
JOINs complejos.
Desventajas:
- Menor consistencia: Al no haber un esquema fijo, los datos pueden volverse inconsistentes si no se gestiona bien la lógica desde el código.
- Consultas complejas: Es más difícil realizar uniones o consultas muy intrincadas que en SQL son triviales con un
JOIN. - Madurez del ecosistema: Aunque son muy populares, el ecosistema SQL tiene décadas de mayor soporte, herramientas consolidadas y estabilidad probada en producción.
Alto rendimiento: FastAPI + MongoDB con Motor
Uno de los objetivos de este capítulo es ayudarte a romper con el esquema mental del SQL relacional para entrar de lleno en el mundo NoSQL. La combinación de FastAPI + MongoDB + Motor no es arbitraria:
- FastAPI es un framework reconocido por su altísimo rendimiento y soporte nativo para operaciones asíncronas con
async/await. - MongoDB es una base de datos diseñada para el alto rendimiento y la escalabilidad horizontal.
- Motor es el driver asíncrono oficial para MongoDB en Python, lo que lo convierte en el complemento perfecto para FastAPI.
Esta combinación es ideal para proyectos de alta demanda donde la velocidad de respuesta y la concurrencia son críticas.
Flexibilidad vs. Consistencia
Uno de los puntos que más repetiré a lo largo de este capítulo es el trade-off entre menor consistencia de datos y mayor flexibilidad. En MongoDB, todo es un JSON (técnicamente BSON — Binary JSON), lo que nos da libertad total en la estructura de los datos, pero también implica una mayor responsabilidad como desarrolladores.
El problema de la consistencia
Imagina que tenemos una tarea con un campo categoria_id. Debido a la flexibilidad de MongoDB, podrías encontrarte con una colección donde coexisten estos tres documentos:
- Una tarea con el campo
categoria_idcorrectamente definido. - Otra tarea que, por un error en el CRUD, no tiene el campo
categoria_id. - Una tarea con etiquetas incrustadas directamente, sin ninguna colección relacional externa.
Si no administras bien tu lógica desde el código, al intentar acceder a la categoría de un documento 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 la calidad de su CRUD que en la propia base de datos.
Conceptos Clave y Equivalencias
Antes de comenzar con el proyecto, es fundamental hablar el mismo idioma. Si vienes del mundo SQL, aquí tienes las equivalencias básicas que usaremos durante todo el capítulo:
| Concepto SQL | Equivalente en MongoDB |
|---|---|
| Tabla | Colección (Collection) |
| Registro / Fila | Documento (Document) |
| Columna | Campo (Field) |
| Clave primaria auto-incremental | ObjectId (generado automáticamente) |
Instalación en Windows
La instalación en Windows es sencilla y directa:
- Busca en Google MongoDB Community Server o accede directamente a
mongodb.com/try/download/community. - Descarga el instalador
.msiy sigue los pasos del asistente (Next → Next → Finish). - Configuración de variables de entorno: Es probable que debas agregar la carpeta
binde la instalación a la variablePATHdel sistema.- Tip: Clic derecho en "Mi Equipo" → Propiedades → Configuración avanzada del sistema → Variables de entorno → selecciona
Path→ Editar → Agregar la ruta de la carpetabin.
- Tip: Clic derecho en "Mi Equipo" → Propiedades → Configuración avanzada del sistema → Variables de entorno → selecciona
- Reinicia el equipo y ya podrás utilizar el comando
mongoshdesde cualquier terminal.
Instalación en macOS (usando Homebrew)
Instalar MongoDB en macOS puede parecer complicado la primera vez, pero con Homebrew el proceso es mucho más limpio y controlado. A continuación 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.
Este es el flujo que uso siempre que configuro un entorno de desarrollo nuevo en Mac. Evita la mayoría de los errores típicos que suelen aparecer al arrancar MongoDB por primera vez.
Requisitos previos
Antes de instalar MongoDB, asegúrate de que tu sistema cuenta con todo lo necesario.
macOS compatible
MongoDB funciona correctamente en versiones modernas de macOS (Catalina en adelante). Si usas una versión muy antigua, se recomienda actualizar el sistema operativo o instalar una versión de MongoDB compatible con tu macOS.
Instalación de Homebrew
macOS no incluye Homebrew por defecto, pero es una de las herramientas más esenciales para desarrollo en Mac. Homebrew es un gestor de paquetes que permite instalar software desde la terminal de forma sencilla y reproducible.
Para instalarlo, sigue las instrucciones oficiales desde su web:
Software necesario para instalar MongoDB
Instalar las Command Line Tools for Xcode
Es muy probable que, al ejecutar cualquier comando de brew por primera vez, 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 de Homebrew.
Agregar el Tap oficial de MongoDB a Homebrew
¿Qué es un tap de Homebrew?
Un tap es simplemente un repositorio adicional que Homebrew usa para encontrar paquetes que no están en su catálogo principal. MongoDB mantiene su propio tap oficial en GitHub (mongodb/homebrew-brew), lo que es importante para evitar instalaciones no soportadas o de terceros.
Comando para agregar el tap
$ brew tap mongodb/brewEste paso es clave. Muchos errores de instalación vienen precisamente de intentar instalar MongoDB sin añadir primero el tap oficial.
Elegir la versión de MongoDB a instalar
MongoDB publica varias versiones estables. En este caso instalaremos una versión específica, que es la que mejor resultado me ha dado en macOS:
$ brew install mongodb-community@8.0También puedes instalar otras versiones estables según tus necesidades:
$ 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 en proyectos en producción.
Interfaz Gráfica: MongoDB Compass
Para trabajar de una manera más cómoda y visual, sin depender exclusivamente de la terminal, instalaremos MongoDB Compass: la herramienta oficial de interfaz gráfica para MongoDB. Es el equivalente a lo que sería TablePlus o DBeaver en el mundo SQL.
- 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
--caskindica que estamos instalando una aplicación con interfaz gráfica, no una herramienta de línea de comandos).
Una vez instalado, lo encontrarás en tu carpeta de Aplicaciones. Ábrelo, conéctate al servidor local (mongodb://localhost:27017) y ya estarás listo para explorar y gestionar tus colecciones visualmente.
También puedes descargarlo directamente desde el instalador oficial:
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. Para verificar que la instalación fue exitosa, puedes comprobar la versión instalada con:
$ mongod --versionIniciar el proceso de MongoDB
Al igual que ocurre con servicios como MySQL, para poder usar MongoDB debes iniciar su servicio de fondo. Para hacerlo, ejecuta:
$ brew services start mongodb-communityEste comando es fundamental. Si intentas conectarte a MongoDB sin haber iniciado el servicio, recibirás un error de conexión 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 servicio, ya puedes acceder al shell de MongoDB con:
$ mongoshSi la conexión es exitosa, verás el prompt de bienvenida:
test>Iniciar el servicio con versión específica
Si instalaste una versión concreta de MongoDB, debes especificarla al arrancar el servicio:
$ brew services start mongodb-community@8.0Si ejecutas mongosh sin haber iniciado el servicio, verás el siguiente error:
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:27017El error ECONNREFUSED indica que el servidor de MongoDB no está activo. Inicia el servicio y vuelve a ejecutar mongosh.
Detener o reiniciar MongoDB
Algunos comandos útiles para gestionar el ciclo de vida del servicio:
$ 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 de MongoDB no está iniciado. Verifica el estado de todos tus servicios de Homebrew con:
$ brew services listBusca mongodb-community en la lista y asegúrate de que el estado sea started.
Problemas de versiones incompatibles
Si actualizaste macOS o cambiaste de versión de MongoDB, puede ser necesario reinstalar la versión correcta o limpiar los servicios antiguos con brew services cleanup antes de volver a instalar.
Motor: El Driver Asíncrono para MongoDB en Python
Para trabajar con MongoDB en Python existen varios conectores, pero en este capítulo utilizaremos Motor, el driver oficial asíncrono mantenido por el propio equipo de MongoDB.
Motor está diseñado específicamente para entornos asíncronos. Como vimos, MongoDB está pensado para grandes volúmenes de datos y alta concurrencia. Si a eso le sumamos el esquema async/await de FastAPI, obtenemos una combinación donde las peticiones se manejan de forma mucho más eficiente: mientras una operación de base de datos espera su respuesta, el servidor puede seguir atendiendo otras solicitudes. Tiene todo el sentido del mundo usar un driver asíncrono cuando el objetivo es precisamente la alta concurrencia.
Instalación de Dependencias
La instalación es sencilla mediante pip. Recuerda tener tu entorno virtual activo antes de ejecutar el comando para no instalar la librería de forma global.
Si vienes del capítulo anterior, en este punto puedes eliminar las dependencias de SQLite y SQLAlchemy del proyecto, ya que MongoDB no las requiere. Nos quedaremos con un stack más limpio y orientado a lo asíncrono.
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 donde 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 continuar, asegúrate de que el servicio de MongoDB esté activo. Si al ejecutar mongosh en tu terminal recibes el prompt de bienvenida (test>), todo está en orden. De lo contrario, debes iniciarlo con brew services start mongodb-community.
Definimos la URL de conexión, que por defecto utiliza el puerto 27017 (similar a como MySQL usa el puerto 3306).
Creamos una función ping_mongo_db_server() para verificar la conexión al arrancar la aplicación. Si el intento de conexión es exitoso, registramos un mensaje informativo en el log; de lo contrario, lanzamos una excepción que detendrá el inicio del servidor y alertará del problema.
Limpieza de SQLAlchemy en FastAPI
En el archivo principal de tu API, debes comentar o eliminar todo lo relacionado con SQLAlchemy y las bases de datos relacionales, ya que ahora utilizaremos MongoDB como única capa de persistencia.
- Importaciones: Elimina o comenta las líneas de
SQLAlchemyy las rutas que dependan de él. - Dependencias: Elimina la función que gestionaba la sesión de la base de datos relacional (generalmente llamada
get_dbo similar). - Modelos: Ya no es necesario crear tablas al inicio, puesto que MongoDB no requiere un esquema fijo predefinido; las colecciones se crean automáticamente al insertar el primer documento.
Ciclo de Vida con Lifespan en FastAPI
Para gestionar la conexión de forma eficiente, utilizaremos el manejador de eventos lifespan de FastAPI. Es una forma moderna y limpia de controlar el ciclo de vida de la aplicación, reemplazando a los antiguos eventos @app.on_event("startup") y @app.on_event("shutdown").
- 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 y verificar la conexión. - Después del
yield: Aquí colocaremos la lógica para cerrar la conexión o liberar recursos cuando la aplicación se detenga.
Configuramos el 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 con uvicorn, deberías ver en la consola el mensaje: "Connected to MongoDB". Esto confirma que la conexión fue exitosa y ya estamos listos para empezar a trabajar con colecciones y documentos.
Obtener el cliente de MongoDB como dependencia
A continuación, creamos un módulo que encapsula el acceso a la base de datos y lo expone como una dependencia inyectable de FastAPI. Este patrón es clave para mantener el código desacoplado y testeable:
mongo_db.py
from db_connection import mongo_client
# Define la base de datos que contendrá todas las colecciones de nuestra aplicación.
# 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 databaseLa función get_mongo_database() será inyectada en nuestros endpoints mediante Depends(), de la misma forma en que se usaba get_db con SQLAlchemy.
Implementación del Router y Esquemas
En el archivo principal de la API, registramos el router bajo el tag Mongo Tasks para mantener organizados nuestros endpoints:
api.py
from mongo_task import mongo_task_router
***
app.include_router(mongo_task_router, prefix="/mongo/tasks", tags=["Mongo Tasks"])Notarás que la estructura es similar a la que usamos con SQLAlchemy. Sin embargo, hay diferencias clave en los métodos disponibles y en cómo se manejan los datos, que iremos viendo a continuación.
Del ORM al Driver Nativo
Con SQLAlchemy usábamos un ORM que abstraía las consultas SQL. En MongoDB, al ser una base de datos documental, trabajamos directamente con el driver nativo, y la nomenclatura cambia:
- En lugar de sentencias SQL, usamos métodos del driver como
insert_one,find,update_oneodelete_one. - Estructura de datos nativa: MongoDB trabaja de forma nativa con
JSON. En Python, esto se traduce en el uso constante de diccionarios (dict) que se mapean directamente a los documentos de la colección.
Operaciones CRUD Paso a Paso
Comencemos con las importaciones necesarias en nuestro archivo de rutas:
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 Pydantic a diccionario con model_dump() e insertamos el registro en la colección. Es una operación asíncrona que nos devuelve el ObjectId generado automáticamente por MongoDB.
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() # Deprecated en Pydantic v2
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),
}El _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 de 24 caracteres 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 y seguro. Sin embargo, MongoDB está diseñado desde su base para ser descentralizado y escalable horizontalmente.
Si tuviéramos varios servidores de MongoDB funcionando en paralelo (un clúster), dos nodos podrían intentar asignar el ID 5 al mismo tiempo, generando un conflicto. El ObjectId soluciona esto de forma elegante combinando varios factores:
- Timestamp: La hora exacta de creación (los primeros 4 bytes), lo que garantiza unicidad en el tiempo.
- Identificador de máquina y contador: Los bytes restantes incluyen un ID de proceso y un contador atómico, lo que asegura la unicidad incluso si dos registros se crean en el mismo milisegundo en el mismo servidor.
2. Leer Todas las Tareas (GET)
Usamos el método find(), que devuelve un cursor (similar a un iterador). Es importante convertirlo a lista mediante to_list() para que FastAPI pueda serializarlo como JSON en la respuesta.
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 antes de consultar la base de datos:
- Validación de formato: Verificamos si el string recibido es un
ObjectIdválido conObjectId.is_valid(). Si no lo es, devolvemos un error400de inmediato para ahorrar un viaje innecesario a la base de datos. - Búsqueda del documento: Si el formato es correcto pero el documento no existe, devolvemos un error
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 usamos model_dump(exclude_none=True) para enviar solo los campos que realmente tienen valor. Para la eliminación, buscamos por _id y ejecutamos delete_one. Si el conteo de documentos afectados es cero, informamos que el registro no fue encontrado con un error 404.
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) # Deprecated en Pydantic v2
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 MongoDB Compass
Una vez ejecutadas las pruebas desde la documentación interactiva de FastAPI (Swagger UI en /docs), puedes refrescar MongoDB Compass y verás cómo los documentos se almacenan con su estructura flexible de tipo JSON, incluyendo el _id de tipo ObjectId.
Este flujo demuestra lo transparente que puede ser la migración de un esquema relacional a uno NoSQL cuando se tiene una arquitectura bien organizada desde el principio.
Relaciones en MongoDB: Esquema Incrustado con Etiquetas
Actualmente tenemos la entidad Task (Tarea). Ahora le vamos a agregar una lista de etiquetas (tags), que será simplemente una lista de strings. Aquí es donde empieza lo interesante: en un esquema relacional puro, las etiquetas vivirían en una tabla separada con una tabla pivote de por medio. En MongoDB, podemos incrustar directamente esa lista dentro del documento de la tarea.
En el modelo de datos (Pydantic), simplemente agregamos el campo tags como lista. No hay tabla adicional, no hay migración, no hay clave foránea:
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] La pregunta natural es: ¿dónde vamos a guardar esas etiquetas si no existe una tabla independiente?
En un modelo relacional, obligatoriamente tendríamos una tabla para tasks, otra para tags y probablemente una tabla pivote task_tags con claves foráneas. En MongoDB no. Aquí rompemos con ese esquema tradicional.
MongoDB almacena documentos
JSON, y unJSONpuede contener unarray. Ese array es precisamente el campotags. Simple y eficiente.
Agregando y Removiendo Etiquetas
En el archivo mongo_task.py añadimos dos endpoints específicos para manipular las etiquetas de una tarea mediante su task_id. La parte de obtener datos e insertar se mantiene igual. Nos enfocaremos en la manipulación de arrays, que es donde MongoDB brilla con sus operadores nativos.
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_task1. Añadir Etiquetas: el Operador $addToSet
Fíjate que recibimos un array de etiquetas de golpe. En un esquema relacional, si quisieras agregar 10 etiquetas, tendrías que hacer 10 inserciones separadas o una operación compleja entre tablas. En MongoDB, todo se hace en una sola operación atómica gracias a los operadores de actualización de arrays.
Usamos update_one con el operador $addToSet:
¿Por qué
$addToSety no$push? El operador$addToSetagrega elementos a un array solo si no existen ya, evitando duplicados de forma atómica. Si usaras$push, podrías terminar con etiquetas repetidas en el mismo documento.
Y para no iterar manualmente con un for en Python (lo cual generaría múltiples operaciones de base de datos), usamos el modificador $each. Esto permite que MongoDB itere internamente y agregue todos los valores del array en una sola escritura.
$addToSet + $each: la combinación perfecta
$addToSet: agrega valores al array sin generar duplicados.$each: itera internamente sobre el array de entrada y aplica$addToSeta cada elemento.
Esta combinación evita tener que:
- Hacer N peticiones a la API (una por etiqueta).
- Iterar manualmente los valores en Python.
- Ejecutar múltiples operaciones de escritura en la base de datos.
2. Remover Etiquetas: el Operador $pull
Para eliminar etiquetas, la lógica es similar pero utilizamos el operador $pull:
- ¿Cómo funciona? Recibimos un array con los valores a remover.
- El operador
$inidentifica cuáles de las etiquetas enviadas existen realmente en el documento. - El operador
$pulllas extrae del array de forma atómica.
Internamente usamos
$pulljunto con$in, que permite comparar múltiples valores en una sola instrucción.
Es muy tolerante a errores: si envías una etiqueta que no existe en la tarea, MongoDB simplemente la ignora sin lanzar ningún error. Esto simplifica mucho la lógica del cliente.
Si revisamos la colección tras algunas operaciones, veremos algo así:
tasks (colecció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 hagas la pregunta: ¿dónde están las etiquetas? En el mundo relacional, tendrías una tabla Tags, una tabla Tasks y probablemente una tabla pivote task_tags. Aquí no existen. Las etiquetas están incrustadas dentro del mismo documento JSON de la tarea.
Esto tiene ventajas y desventajas claras:
- Lo bueno: Cuando consultas una tarea, ya traes sus etiquetas en la misma operación, sin necesidad de hacer ningún
JOIN. Esto es especialmente eficiente en lecturas intensivas. - Lo malo: No hay un sistema de migraciones estricto. Como viste en el ejercicio, agregamos el campo
tagsal modelo y MongoDB simplemente empezó a guardarlo en los documentos nuevos o actualizados. Los documentos anteriores sin ese campo seguirán existiendo tal cual, lo que puede generar inconsistencias si no se maneja adecuadamente en el código.
Cómo se almacenan las etiquetas en MongoDB
MongoDB almacena documentos en formato JSON (internamente BSON):
{
"id": 1,
"title": "Tarea 1",
"tags": ["tag1", "tag2"]
}Las etiquetas están incrustadas dentro del mismo documento de la tarea. No existe ninguna tabla, colección ni referencia externa. Cuando consultas la tarea, ya obtienes todas sus etiquetas en una sola operación de lectura.
Relaciones en MongoDB
Vamos a consolidar lo aprendido. En el ejemplo anterior estamos trabajando con una relación de tipo Muchos a Muchos (N:N), aunque de una forma no convencional para quienes vienen del mundo SQL.
¿Por qué es Muchos a Muchos? Veámoslo en la práctica:
Tenemos una tarea con las etiquetas Tag 1, Tag 3 y Tag 4, y otra tarea sin ninguna. Si agregamos 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 clave foránea ni índices relacionales." Y aquí es donde debes abrir la mente: MongoDB no es una base de datos relacional. El vínculo no es un ID que apunta a otra tabla, sino que el valor mismo es el vínculo. Si el string "Tag 3" es idéntico en ambos documentos, existe una relación funcional. MongoDB es, en esencia, un gestor que nos permite manipular esos JSON con una flexibilidad que un esquema SQL no puede igualar.
Esquemas Normalizados vs. Desnormalizados
En MongoDB tenemos dos estrategias principales para estructurar las relaciones entre datos:
1. Esquema Desnormalizado (Incrustado / Embedded)
Es el que estamos usando. Guardamos el valor directamente (el texto de la etiqueta) dentro del documento de la tarea.
- Ventaja: No necesitas colecciones intermedias ni operaciones tipo
JOIN. Al traer la tarea, ya tienes toda la información en una sola lectura. - Ideal para: Datos que se leen frecuentemente juntos y que no cambian de forma independiente.
2. Esquema Normalizado (Referenciado / Referenced)
Es el equivalente al esquema relacional. En lugar de guardar el texto "Tag 3" directamente, guardamos el ObjectId que hace referencia a un documento en una colección separada llamada tags.
- Estructura: Tendrías un array de
ObjectIds llamadotag_idsdentro del documento de la tarea. - Cuándo usarlo: Se recomienda cuando la entidad relacionada sufre muchas actualizaciones independientes. Si cambias el nombre de una etiqueta en su propia colección, el cambio se refleja automáticamente en todos los documentos que la referencian, porque estos solo guardan el
ObjectId.
¿Cuándo usar cada estrategia? (1:1, 1:N y N:N)
La decisión depende de tu lógica de negocio y de la frecuencia de actualización de los datos relacionados:
- 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
categorycon el nombre (desnormalizado) o unObjectIdde referencia a la coleccióncategories. - Relación Uno a Uno (1:1): Ejemplo clásico: Usuarios y Direcciones. Como una dirección suele ser única para un usuario y se accede siempre junto a él, lo más lógico es que el esquema de la dirección viva embebido dentro del objeto
user. No tiene sentido crear una colección separada para algo que no va a ser compartido ni consultado de forma independiente.
Operaciones Masivas: update_one vs update_many
Si usas el esquema desnormalizado y necesitas renombrar una etiqueta en todas las tareas que la contienen, no puedes usar update_one (que solo afecta a un único documento). 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 con un filtro que busque ese valor y lo reemplace en toda la colección de un solo golpe, sin necesidad de iterar desde el código.
Conclusión y Práctica
La mejor forma de asimilar MongoDB es romper definitivamente con el esquema mental de las tablas relacionales. No pienses en filas y columnas; piensa en documentos JSON anidados y flexibles.
Como ejercicio, te propongo que intentes implementar el código para un update_many siguiendo la estructura de este proyecto. También puedes consultar el repositorio oficial para comparar tu solución con la mía. Además, prueba a crear un modelo donde las direcciones sean un objeto embebido dentro del documento de un usuario, o simula una relación 1:N con categorías usando referencias (ObjectId). 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