Relaciones Uno a Muchos Referenciadas en MongoDB

Clase 22 de 24Curso de NestJS: Persistencia de Datos con MongoDB

Contenido del curso

Mongoose

Resumen

Cuando trabajas con bases de datos NoSQL como MongoDB, una de las decisiones más importantes es cómo modelar las relaciones entre colecciones. En lugar de embeber subdocumentos completos dentro de un array, puedes almacenar únicamente los IDs de referencia y resolver esas relaciones mediante joins. Esto resulta fundamental para escenarios como un carrito de compras, donde una orden necesita referenciar múltiples productos sin duplicar información.

¿Cómo se define una relación uno a muchos referenciada en la entity?

El punto de partida es la OrderEntity, ubicada dentro del módulo de users en una arquitectura modular de NestJS [01:00]. Esta entidad ya cuenta con una relación uno a uno embebida hacia Customer, es decir, cada orden está vinculada a un cliente específico.

Para agregar productos de forma referenciada, se declara un atributo products cuyo tipo es un array de ObjectId [01:52]. A diferencia de la relación embebida, donde se usaba Record<string, any>, aquí existe un schema directo con el cual relacionar: el de Product. Se importa desde el módulo de productos y se configura así:

  • El type del array se establece como Types.ObjectId de Mongoose.
  • Se indica la referencia con ref: Product.name.
  • El array queda envuelto dentro de la definición del schema como un arreglo de objetos.

De esta forma, MongoDB almacena solo los identificadores y la relación queda tipada correctamente en TypeScript [02:40].

¿Qué problema surge con el tipado y cómo se resuelve con OmitType?

Al definir el DTO para crear una orden, se valida que products sea un array de strings (los IDs en formato texto) usando decoradores como @IsArray() e @IsNotEmpty() [03:20]. Sin embargo, aparece un conflicto de tipos: la entity declara products como un array de objetos Product, mientras el DTO lo recibe como un array de strings.

Este choque genera un error de tipado al momento de actualizar [04:05]. La solución consiste en no permitir la edición de productos a través del endpoint de actualización general, ya que la manipulación de arrays en MongoDB debe hacerse con métodos específicos.

Para lograrlo se usa OmitType, uno de los mapped types que ofrece NestJS junto con PartialType [05:00]. La combinación funciona así:

  • OmitType(CreateOrderDTO, ['products']) genera un tipo sin el campo products.
  • PartialType(...) envuelve ese resultado para que los campos restantes sean opcionales.

typescript export class UpdateOrderDTO extends PartialType( OmitType(CreateOrderDTO, ['products']), ) {}

Con esto se elimina el conflicto de tipos y la edición queda limitada a campos como customer y date [05:45].

¿Cómo se crea una orden y se resuelven las referencias con populate?

Una vez configurada la entity y el DTO, se prueba la creación en Insomnia enviando un JSON con tres campos [07:00]:

  • date: fecha en formato estándar ISO.
  • customer: el ID de un cliente existente en la colección de customers.
  • products: un array con los IDs de productos existentes.

Al enviar la petición POST hacia /orders, MongoDB almacena la orden con las referencias como ObjectId. Esto se puede verificar en MongoDB Compass, donde la colección orders muestra los IDs almacenados [08:15].

¿Por qué es necesario usar populate para resolver los joins?

Si se hace un GET a /orders sin configurar nada adicional, la respuesta devuelve únicamente IDs crudos, algo inútil para mostrar en una interfaz [08:50]. Para resolver las relaciones se utiliza el método populate de Mongoose en el servicio:

typescript this.orderModel.find() .populate('customer') .populate('products') .exec();

Cada llamada a .populate() indica a Mongoose que debe hacer un join con la colección referenciada [09:25]. Al probar nuevamente el GET, la respuesta incluye los objetos completos: el array de products con todos sus atributos y el customer con sus datos, incluidas sus skills.

¿Cuál es la diferencia práctica entre embebido y referenciado?

  • Embebido: los subdocumentos viven dentro del documento padre. Ideal para datos que siempre se consultan juntos.
  • Referenciado: se almacenan solo IDs y se resuelven con populate. Perfecto cuando los datos referenciados cambian con frecuencia o se comparten entre múltiples documentos.

En el caso de una orden de compras, la forma referenciada permite que los productos mantengan su propia colección y se actualicen de forma independiente [09:55].

Ahora que sabes crear relaciones referenciadas y resolver joins, el siguiente paso natural es aprender a agregar y remover elementos individuales dentro de esos arrays, una operación que requiere métodos específicos de MongoDB para seguir las buenas prácticas. ¿Has tenido que decidir entre embebido y referenciado en algún proyecto? Comparte tu experiencia.