Aller au contenu

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 :

  1. 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().
  2. 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().
  3. 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.

bash
     nest g resource profiles
     nest g resource users

Voici la sortie pour la création de la ressource profiles :

Création d'une ressource Nest

Explorons user.entity.ts :

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 UserRole est 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 :

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 :

profile.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;

  @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

user.service.ts
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.

bash
     nest g resource orders
user.entity.ts
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[];
}
order.entity.ts
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 :

order.Service.ts
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 :

order.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 @JoinTable sur 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 :

order.Service.ts
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 :

order.Service.ts
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.

Propulsé par Buttondown.

Related posts

© 2026 < Denis AKPAGNONITE /> | N1BBzerLZXT