Maîtriser NestJS : libérer la puissance des relations avec TypeORM et les bases SQL
18 octobre 2023 · 6 min read · Read in English
Sommaire
Introduction
Bienvenue dans « Maîtriser NestJS : libérer la puissance des relations avec TypeORM et les bases SQL ». Dans cet article, on regarde comment NestJS, conjointement avec TypeORM et les bases SQL, peut t'aider à construire des structures de données complexes et à gérer leurs interactions. À la fin de ce tutoriel, tu seras capable de concevoir des APIs qui gèrent des connexions de données complexes avec aisance, et tu feras passer ta maîtrise de NestJS au niveau supérieur.
Que tu sois un développeur NestJS expérimenté qui veut élargir ses connaissances ou un débutant impatient de maîtriser les subtilités des relations de données, cette exploration complète te donnera l'expérience nécessaire pour construire des applications à la pointe. Alors, c'est parti — apprenons à concevoir des systèmes solides et interconnectés avec NestJS, TypeORM et les bases SQL.
J'ai créé un dépôt GitHub pour cette série, accessible à l' adresse suivante.
C'est quoi les relations
Les relations sont des connexions formées entre deux tables ou plus. Elles s'établissent via des champs communs aux tables, qui incluent souvent des clés primaires et étrangères.
Il existe trois types de relations :
- One-to-one : chaque ligne de la table primaire a exactement une ligne dans la table
étrangère. Pour définir ce type de relation, on utilise le décorateur
@OneToOne(). - One-to-many / Many-to-one : chaque ligne de la table primaire est connectée à une ou
plusieurs lignes de la table étrangère. Pour définir ce type de relation, on utilise les
décorateurs
@OneToMany()et@ManyToOne(). - Many-to-many : chaque ligne de la table primaire a plusieurs lignes liées dans la table
étrangère, et chaque enregistrement de la table étrangère a plusieurs lignes liées dans la
table primaire. On utilise
@ManyToMany()pour définir ce type de relation.
On va passer en revue chacun de ces termes en détail.
One-To-One
One-to-one est une relation où A contient une seule instance de B, et B contient une seule instance de A. Prenons par exemple les entités User et Profile. Un utilisateur ne peut avoir qu'un seul profil, et un profil n'appartient qu'à un seul utilisateur.
Puisqu'on va implémenter l'authentification et l'autorisation dans le prochain article, on commence par créer les ressources liées. Créons donc les ressources REST API profiles et users.
nest g resource profiles
nest g resource users
Voici la sortie pour la création de la ressource profiles :
Explorons user.entity.ts :
import { UserRole } from 'src/enums/user.role';
import { Profile } from 'src/profiles/entities/profile.entity';
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.CUSTOMER })
role: string;
}
L'enum
UserRoleest disponible dans le code source. Tu devrais y jeter un œil si tu veux voir comment il est implémenté.
Maintenant explorons le contenu de profile.entity.ts :
import { User } from 'src/users/entities/user.entity';
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('profiles')
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
full_address?: string;
@Column({ nullable: true })
photo?: string;
@OneToOne(() => User, (user) => user.profile) // specify inverse side as a second parameter
@JoinColumn()
user: User;
}
Ci-dessus, on a utilisé le décorateur @OneToOne(). Son argument est une fonction qui renvoie
la classe de l'entité avec laquelle on souhaite établir une relation.
Le décorateur @JoinColumn() spécifie que la relation est détenue par l'entité Profile. Il
signifie que les lignes de la table Profile contiennent la colonne userId, qui peut stocker
l'id d'un utilisateur. On ne l'utilise que d'un côté de la relation.
Relation bidirectionnelle
Notre relation est actuellement unidirectionnelle. Ça signifie qu'un seul côté de la relation connaît l'autre. Avec TypeORM, les relations peuvent être uni- ou bidirectionnelles. Les unidirectionnelles n'ont un décorateur de relation que d'un seul côté. Les bidirectionnelles en ont des deux côtés.
On vient de créer une relation unidirectionnelle. Rendons-la bidirectionnelle :
import { UserRole } from 'src/enums/user.role';
import { Profile } from 'src/profiles/entities/profile.entity';
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.CUSTOMER })
role: string;
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
}
À noter : la relation inverse est une idée plutôt abstraite qui n'ajoute pas de nouvelles colonnes à la base.
On vient de rendre nos relations bidirectionnelles. Attention, la relation inverse n'a pas de
@JoinColumn. @JoinColumn ne doit être que d'un seul côté de la relation — sur la table qui
contient la clé étrangère.
Sauvegarder et récupérer une relation one-to-one
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { Profile } from 'src/profiles/entities/profile.entity';
import { UpdateProfileDto } from 'src/profiles/dto/update-profile.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(Profile) private profileRepository: Repository<Profile>,
) {
}
async create(createUSerDto: CreateUserDto) {
const user = await this.userRepository.save(createUSerDto); // saving the user
// creating the the profile object
const profile = new Profile();
profile.full_address = createUSerDto.full_address;
profile.photo = createUSerDto.photo;
profile.user = user;
await this.profileRepository.save(profile); // linking the profile to user
return this.findOne(user.id); // return the user with the profile
}
async findOne(id: number) {
const user = await this.userRepository.findOne({
where: { id },
relations: { profile: true }, // by doing this, we're implementing the eager loading to automatically load the profile object
});
if (!user) throw new HttpException('user not found', HttpStatus.NOT_FOUND);
return user;
}
}
Many-To-One / One-To-Many
Many-to-one / one-to-many est une relation où A contient plusieurs instances de B, mais B ne contient qu'une seule instance de A. Prenons par exemple les entités User et Order. Un utilisateur peut avoir plusieurs commandes, mais chaque commande n'appartient qu'à un seul utilisateur.
Mettons rapidement en place la ressource REST API orders.
nest g resource orders
import { Order } from 'src/orders/entities/order.entity';
@Entity('users')
export class User {
// we add the following to user entity
@OneToMany(() => Order, (order) => order.user)
orders: Order[];
}
import { OrderStatus } from 'src/enums/order.status';
import { User } from 'src/users/entities/user.entity';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'enum', enum: OrderStatus, default: OrderStatus.PLACED })
status: string; // the order status
@Column()
amount: number;
@ManyToOne(() => User, (user) => user.orders)
user: User;
@CreateDateColumn()
created_datetime?: Date;
}
On a ajouté @OneToMany à la propriété orders et défini Order comme le type de la relation
cible. Dans une relation @ManyToOne / @OneToMany, tu peux omettre @JoinColumn.
@ManyToOne ne peut pas exister sans @OneToMany. @ManyToOne est requis si tu veux
utiliser @OneToMany. Cependant, si seul @ManyToOne t'intéresse, tu peux le définir sans
avoir @OneToMany sur l'entité associée. Partout où @ManyToOne est configuré, son entité
liée aura un « relation id » et une clé étrangère.
Sauvegarder et récupérer une relation one-to-many / many-to-one
Voici le service order complet qui implémente un CRUD sur les commandes :
import { Injectable } from '@nestjs/common';
import { CreateOrderDto } from './dto/create-order.dto';
import { UpdateOrderDto } from './dto/update-order.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';
import { Repository } from 'typeorm';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class OrdersService {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly userService: UsersService,
) {
}
async create(userId: number, createOrderDto: CreateOrderDto) {
const user = await this.userService.findOne(userId);
const order = new Order();
order.amount = createOrderDto.amount;
order.user = user;
return await this.orderRepository.save(order);
}
async findAll(userId: number): Promise<Order[]> {
const user = await this.userService.findOne(userId);
return await this.orderRepository.find({
where: { user },
});
}
async findOne(id: number): Promise<Order | null> {
return await this.orderRepository.findOneBy({ id });
}
}
Relations Many-To-Many
Many-to-many est une relation dans laquelle l'entité A contient plusieurs instances de l'entité B, et inversement. Par exemple, considérons les entités Product et Order. Une commande peut inclure plusieurs produits, et chaque produit peut appartenir à plusieurs commandes.
Explorons le contenu de profile.entity.ts :
import { Product } from 'src/products/entities/product.entity';
import { ManyToMany, JoinTable } from 'typeorm';
@Entity('orders')
export class Order {
// ..... other properties
// our new property
@ManyToMany(() => Product)
products: Product[];
}
@JoinTable()est requis pour les relations@ManyToMany. Tu dois mettre@JoinTablesur un seul côté (le côté propriétaire) de la relation.
Sauvegarder des relations many-to-many
Mettons à jour le fichier de service order et ajoutons quelques lignes à la méthode create :
export class OrdersService {
// ....previous stuffs
async create(userId: number, createOrderDto: CreateOrderDto) {
// ..... previous stuffs
const products = [];
for (const product of createOrderDto.products) {
try {
const dbProduct = await this.productService.findOne(product);
products.push(dbProduct);
} catch (error) {
// We'll update this later
console.log('failed to find product with id ' + product);
}
}
order.products = products;
// .... previous stuffs
}
}
Charger des relations many-to-many
Pour charger des commandes avec leurs produits, tu dois spécifier la relation dans les FindOptions :
export class OrdersService {
// ....previous stuffs
async findOne(id: number): Promise<Order | null> {
return await this.orderRepository.findOne({
relations: {
products: true,
},
where: { id },
});
}
}
Résumé
On a plongé en profondeur dans le monde des relations de données avec NestJS, TypeORM et les bases SQL dans cet article étendu. Ce cours t'aidera, que tu sois un développeur NestJS expérimenté qui veut élargir son expertise ou un débutant désireux d'apprendre les nuances des relations de données. Reste à l'écoute pour le prochain article, où on plongera dans la validation des données et la gestion d'erreurs, en t'équipant d'encore plus d'outils pour devenir un expert NestJS. Rejoins-moi dans cette aventure, et maîtrisons ensemble l'art des relations de données dans NestJS !
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
Maîtriser NestJS : connecter NestJS à une base PostgreSQL
Apprends les étapes essentielles pour maîtriser NestJS et tirer parti de TypeORM pour une connectivité base de données robuste dans tes projets.
September 6, 2023
Maîtriser NestJS : comprendre Services, Modules et Contrôleurs
Apprends les composants essentiels de NestJS pour construire des applications serveur scalables. Examine le rôle des services, modules et contrôleurs en exploitant les capacités des DTO (Data Transfer Objects) pour la validation et le renforcement de la sécurité.
September 3, 2023
Maîtriser NestJS : ton guide ultime du développement backend moderne
Explore NestJS, un framework Node.js moderne, dans cette introduction complète. Rejoins notre série pour maîtriser l'architecture modulaire, les contrôleurs, providers, services, et plus encore. Construis des applications backend scalables en toute confiance. Bienvenue dans la partie 1 de la série NestJS !
August 23, 2023