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.
¿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:
- Productos
- Reviews
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)
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".
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.
- El constructor define dos propiedades productsRepository y reviewRepository para poder trabajar con ambas entidades.
- El método saveReview recibe el identificador de producto y lo primero que hace es buscar el producto que tiene ese identificador.
- Si encuentra el producto, entonces puede insertar la review. Si no lo encuentra, simplemente levanta una excepción.
- Para insertar la review comienza por crear un nuevo elemento con el método create() del reviewRepository.
- Luego, asigna el producto a la review creada con
review.product = product
. Realmente, todo el trabajo para definir el objeto relacionado se hace en esta línea y el propio ORM será el que se encargue de guardar todo convenientemente en la tabla. - El método lo tenemos que firmar como async porque se hace dentro un par de awaits, el primero para esperar a que termine la búsqueda del producto y el segundo esperando que termine el almacenamiento del review.
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]
})
Conclusión
En este artículo hemos visto cómo trabajar con relaciones de 1 a N, que es el tipo de relación más habitual en las bases de datos. Hemos comprobado que TypeORM nos permite de una manera sencilla definir la relación en ambos sentidos, con los decoradores @OneToMany
y @ManyToOne
.
Sin embargo, este es uno de los varios tipos de realaciones que podemos implementar en las bases de datos y por supuesto en Nest y TypeORM. En el siguiente artículo estudiaremos las relaciones de muchos a muchos.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...