> Manuales > Manual de NestJS

Cómo trabajar con relaciones de uno a muchos, o de muchos a uno, usando TypeORM en aplicaciones Nest. Veremos cómo se definen las relaciones en las entidades y cómo luego insertamos datos de las entidades relacionadas.

Relación de uno a muchos con TypeORM y NestJS

¿Qué sería de un ORM sin implementar facilidades para las relaciones entre tablas?

Por supuesto, como podrás imaginar, las relaciones entre tablas son una de las funciones básicas de TypeORM y que vamos a abordar en este manual de Nest.

Como el tema de las relaciones entre tablas es bastante amplio, lo tendremos que abordar en distintos artículos. En el presente vamos a hablar de un tipo de relación sencillo pero habitual, como es la relación entre 1 y muchos.

Si no tienes claro cómo y por qué se definen las relaciones entre las tablas, o quieres saber más sobre los tipos de relaciones en el modelo relacional, te recomendamos acceder a la categoría dedicada a bases de datos.

Relaciones de 1 a muchos

Pero antes de meternos en los detalles de la implementación en TypeORM y Nest, vamos a hablar brevemente del tipo de relación que nos ocupa.

Dentro de las posibles relaciones de las tablas de bases de datos una de las más frecuentes que nos encontramos es la de "1 a muchos" (también llamada de "1 a N"). Esta relación se da por ejemplo en los comentarios de un post, en las facturas de un cliente, o en las fotos del perfil de un usuario. Existen innumerables ejemplos.

La relación de "1 a N" también tiene su inversa, es decir de "N a 1". En realidad se trata del mismo tipo de relación, pero vista en sentido contrario. Es decir, para la relación entre los comentarios y los post, si la vemos desde el punto de vista de los post diríamos que es de 1 a muchos (un comentario tiene muchos post) y si la vemos desde el punto si los comentarios, diríamos que es de muchos a 1, ya que muchos post pueden estar en un comentario.

En la documentación de TypeORM cuando explica los tipos de relaciones en realidad las menciona de manera separada, aunque podamos pensar que es la misma, y es que su implementación difiere dependiendo qué sentido de la relación necesitamos recorrer. Todo lo vamos a ver con un ejemplo a continuación.

Entidades de nuestro ejemplo

Imaginemos que queremos almacenar las valoraciones y la puntuación que los visitantes dan a los productos. Entonces tenemos dos entidades:

Si pensamos en los productos estamos ante de una relación de 1 a muchos, porque 1 producto pueden tener varias reviews.

Sin embargo, si pensamos en los reviews, esta misma relación sería de muchos a 1, porque muchos reviews pueden estar en un artículo.

En todo caso, en este tipo de relaciones, del modelo entidad-relación, las tablas que mantienen la entidad "N" (muchos) obtiene el identificador del elemento relacionado de la tabla "1".

La clave o identifiador del elemento que se encuentra relacionado toma el nombre de clave foránea (foreign key en inglés)

En el ejemplo que nos ocupa, 1 product tiene relación con "N" reviews, de modo que la tabla de reviews tendrá el identificador del post al que pertenece. Dicho de otro modo, la clave primaria de products aparecerá en la tabla reviews como clave foránea.

TypeORM es capaz de crear para ti las tablas y por supuesto los campos de la relación. más tarde explicamos cómo lo haremos, así que de momento no necesitas preocuparte de los detalles técnicos de los identificadores de las tablas.

Cómo definir una relación en la entidad de TypeORM

Tal como estamos acostumbrados, usamos decoradores para definir la estructura de nuestras entidades. Existen decoradores específicos para definir cada una de las relaciones.

@OneToMany()

Comenzamos por @OneToMany(), que es el decorador que nos sirve para definir las relaciones de 1 a muchos.

Dado nuestro ejemplo de productos y reviews, en la entidad del producto tendremos que definir la relación con los reviews con el decorador @OneToMany(). Para implementar esta relación tenemos que alimentar al decorador con dos funciones que nos ayudarán a especificar los campos mediante los cuales está relacionada la entidad.

@Entity('products')
export class Product {
	
  // [...]

  @OneToMany(() => Review, review => review.product)
  reviews: Review[];
}

En la primera función indicamos la entidad con la que nos estamos relacionando. En este caso, desde "Product" nos relacionamos con "Review".

En la segunda función indicamos qué campo de la entidad relacionada apuntaría a la entidad que se está implementando. Es decir, qué campo de la entidad "Review" apunta a la entidad de "Product". Para ello se recibe un objeto review y se devuelve la propiedad de este objeto que apunta a su relacionada.

A continuación, el tipo de campo decorado se indica con el tipo de array de la entidad relacionada. Es un array porque un producto puede almacenar más de un review.

reviews: Review[];

@ManyToOne()

El decorador @ManyToOne() lo usamos para definir relaciones de muchos a 1.

En nuestro ejemplo lo usamos en la implementación de la entidad Review, para relacionarnos con la entidad Product. Seguimos alimentando al decorador @ManyToOne() con dos funciones para especificar el funcionamiento de la relación.

@Entity('reviews')
export class Review {
  
  [...]

  @ManyToOne(() => Product, product => product.reviews)
  product: Product;
}

La primera función nos sirve para indicar el tipo de la entidad con la que estamos relacionando. Es decir, en la implementación de un Review nos vamos a relacionar con la entidad Product.

La segunda función nos sirve para que se sepa el campo de la entidad relacionada que apunta a esta relación. Es decir, desde product usaremos el campo "reviews" para encontrar la entidad implementada. Para ello recibimos un objeto de producto y devolvemos la propiedad donde encontramos las entidades review.

Por último, definimos el nombre del campo que te llevará a la entidad relacionada, es decir, existirá un campo "product" dentro del objeto de entidad "Review" que contendrá un elemento de la entidad "Product". En este caso no es un array, porque la review solo está relacionada con un único producto.

product: Product;

Implementación de las tablas

Una vez aplicados los decoradores con las configuraciones adecuadas, han quedado definidas las dos relaciones entre nuestras entidades. Ahora estamos estamos en condiciones de iniciar la aplicación y comprobar si está funcionando todo correctamente.

Si no da ningún error de arranque tendremos que verificar cómo se han creado las tablas y si contienen los campos necesarios para implementar este tipo de relación.

Recuerda que en la configuración de TypeORM hemos indicado synchronize: true que indicaba que se tienen que crear las tablas con los campos que se hayan definido en las implementaciones de las entidades.

En las relaciones de 1 a muchos, la tabla que implementa el "muchos" debe tener el campo de identificación de la tabla con la que se relaciona. Es decir, los reviews tendrán el campo id del producto al que pertenecen. Esa columna de la tabla se crea de manera automática con un nombre definido por el propio ORM.

Esta imagen te muestra cómo estaría definida la tabla "reviews".

Cómo queda la tabla definida y la clave foranea de la relación

Como puedes ver, se ha creado una columna llamada "productId" que es un entero, donde se almacenará el identificador del producto relacionado.

Cómo usar TypeORM para guardar las relaciones

Ahora vamos a ver un ejemplo sobre cómo podemos crear un elemento de la entidad "Review" al que vamos a relacionar con un elemento de la entidad Product.

Para ello vamos a crear una ruta de controlador que recibe un identificador de producto y el cuerpo de un review.

@Post(':id/review')
async createReview(
  @Param('id', ParseIntPipe) id: number,
  @Body() body: ReviewDto,
) {
  return this.reviewsService.saveReview(id, body);
}

Este método llama a reviewsService y le pide que inserte la review, pasando por parámetro el identificador del producto y los datos de la review.

Por su parte, reviewService, que es donde hacemos el trabajo de crear la review y relacionarla con el producto, tendrá este código.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
import { Review } from './entities/review.entity';
import { ReviewDto } from './dto/review.dto';

@Injectable()
export class ReviewsService {

  constructor(
    @InjectRepository(Product)
    private productsRepository: Repository<Product>,
    @InjectRepository(Review)
    private reviewRepository: Repository<Review>,
  ) { }

  async saveReview(id: number, body: ReviewDto) {
    const product = await this.productsRepository.findOne(id);
    console.log(product, id);
    if (product) {
      const review = this.reviewRepository.create(body);
      review.product = product;
      await this.reviewRepository.save(review);
      return review
    }
    throw new NotFoundException(`No encontramos el producto ${id}`)
  }
}

Como puedes ver, se trata de un nuevo servicio, al que hemos colocado dos métodos. Un constructor y un método de salvar la review. Vamos a enumerar los puntos del código que merecen más atención.

Recueda además que, para poder usar el reviewRepository en el servicio, es necesario hacer la declaración de esta entidad en el "imports" del módulo.

En este caso, el decorador @Module() de products.module.ts, tendrá este código, en el que usamos TypeOrmModule, tal como aprendimos al hablar de las entidades de TypeORM.

@Module({
  imports: [TypeOrmModule.forFeature([Product, Review])],
  controllers: [ProductsController, ReviewsController],
  providers: [ProductsService, ReviewsService]
})

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual