NestJS Logo

Base de données

Nest est agnostique en matière de bases de données, ce qui vous permet de l'intégrer facilement à n'importe quelle base de données SQL ou NoSQL. Vous disposez d'un certain nombre d'options, en fonction de vos préférences. Au niveau le plus général, connecter Nest à une base de données consiste simplement à charger un pilote Node.js approprié pour la base de données, comme vous le feriez avec Express ou Fastify.

Vous pouvez également utiliser directement n'importe quelle bibliothèque d'intégration de base de données Node.js ou ORM, comme MikroORM (voir MikroORM recipe), Sequelize (voir l'intégration Sequelize), Knex.js (voir le tutoriel Knex.js), TypeORM, et Prisma (voir la Recette Prisma), pour opérer à un niveau d'abstraction plus élevé.

Par commodité, Nest fournit une intégration étroite avec TypeORM et Sequelize avec les packages @nestjs/typeorm et @nestjs/sequelize respectivement, que nous couvrirons dans le chapitre actuel, et Mongoose avec @nestjs/mongoose, qui est couvert dans cet autre chapitre. Ces intégrations fournissent des fonctionnalités supplémentaires spécifiques à NestJS, telles que l'injection de modèle/référentiel, la testabilité, et la configuration asynchrone pour rendre l'accès à la base de données choisie encore plus facile.

Intégration TypeORM

Pour l'intégration avec les bases de données SQL et NoSQL, Nest fournit le package @nestjs/typeorm. TypeORM est l'ORM (Object Relational Mapper) le plus mature disponible pour TypeScript. Comme il est écrit en TypeScript, il s'intègre bien au framework Nest.

Pour commencer à l'utiliser, nous installons d'abord les dépendances nécessaires. Dans ce chapitre, nous allons démontrer l'utilisation du populaire SGBD relationnel MySQL, mais TypeORM fournit un support pour de nombreuses bases de données relationnelles, telles que PostgreSQL, Oracle, Microsoft SQL Server, SQLite, et même des bases de données NoSQL comme MongoDB. La procédure décrite dans ce chapitre est la même pour toutes les bases de données supportées par TypeORM. Vous devrez simplement installer les bibliothèques API client associées à la base de données que vous avez sélectionnée.


$ npm install --save @nestjs/typeorm typeorm mysql2

Une fois le processus d'installation terminé, nous pouvons importer le TypeOrmModule dans la racine AppModule.

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}
Attention Le paramètre synchronize : true ne doit pas être utilisé en production - sinon vous pouvez perdre des données de production.

La méthode forRoot() supporte toutes les propriétés de configuration exposées par le constructeur DataSource du package TypeORM. En outre, il existe plusieurs propriétés de configuration supplémentaires décrites ci-dessous.

retryAttemptsNombre de tentatives de connexion à la base de données (par défaut : 10)
retryDelayDélai entre les tentatives de reconnexion (ms) (par défaut : 3000)
autoLoadEntitiesSi true, les entités seront chargées automatiquement (par défaut : false)
Astuce Apprenez-en plus sur les options de source de données ici.

Une fois cela fait, les objets TypeORM DataSource et EntityManager seront disponibles pour être injectés dans l'ensemble du projet (sans avoir besoin d'importer des modules), par exemple :

app.module.ts
JS TS

import { DataSource } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(private dataSource: DataSource) {}
}

import { DataSource } from 'typeorm';

@Dependencies(DataSource)
@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }
}

Modèle de répertoire#

TypeORM prend en charge le modèle de conception de répertoire, de sorte que chaque entité dispose de son propre répertoire. Ces répertoires peuvent être obtenus à partir de la source de données de la base de données.

Pour continuer l'exemple, nous avons besoin d'au moins une entité. Définissons l'entité User.

user.entity.ts
JS TS

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}
Astuce Pour en savoir plus sur les entités, consultez la documentation TypeORM.

Le fichier d'entité User se trouve dans le répertoire users. Ce répertoire contient tous les fichiers relatifs au module Users. Vous pouvez décider de l'endroit où vous voulez garder vos fichiers de modèle, cependant, nous recommandons de les créer près de leur domaine, dans le répertoire du module correspondant.

Pour commencer à utiliser l'entité User, nous devons la faire connaître à TypeORM en l'insérant dans le tableau entities dans les options de la méthode forRoot() du module (à moins que vous n'utilisiez un chemin global statique) :

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Ensuite, regardons le module UsersModule :

users.module.ts
JS TS

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

Ce module utilise la méthode forFeature() pour définir quels référentiels sont enregistrés dans le scope courant. Avec cela en place, nous pouvons injecter le UsersRepository dans le UsersService en utilisant le décorateur @InjectRepository() :

users.service.ts
JS TS

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

import { Injectable, Dependencies } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

@Injectable()
@Dependencies(getRepositoryToken(User))
export class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  findAll() {
    return this.usersRepository.find();
  }

  findOne(id) {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id) {
    await this.usersRepository.delete(id);
  }
}
Remarque N'oubliez pas d'importer le module UsersModule dans le module racine AppModule.

Si vous voulez utiliser le référentiel en dehors du module qui importe TypeOrmModule.forFeature, vous devrez réexporter les fournisseurs générés par ce module. Vous pouvez le faire en exportant le module entier, comme ceci :

users.module.ts
JS TS

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule]
})
export class UsersModule {}

Maintenant, si nous importons UsersModule dans UserHttpModule, nous pouvons utiliser @InjectRepository(User) dans les fournisseurs de ce dernier module.

users-http.module.ts
JS TS

import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

Relations#

Les relations sont des associations établies entre deux ou plusieurs tables. Les relations sont basées sur des champs communs à chaque table, impliquant souvent des clés primaires et étrangères.

Il existe trois types de relations :

One-to-oneChaque ligne de la table primaire a une et une seule ligne associée dans la table étrangère. Utilisez le décorateur @OneToOne() pour définir ce type de relation.
One-to-many / Many-to-oneChaque ligne de la table primaire a une ou plusieurs lignes liées dans la table étrangère. Utilisez les décorateurs @OneToMany() et @ManyToOne() pour définir ce type de relation.
Many-to-manyChaque ligne de la table primaire a plusieurs lignes apparentées dans la table étrangère, et chaque enregistrement de la table étrangère a plusieurs lignes apparentées dans la table primaire. Utilisez le décorateur @ManyToMany() pour définir ce type de relation.

Pour définir des relations dans les entités, utilisez les décorateurs correspondants. Par exemple, pour définir que chaque User peut avoir plusieurs photos, utilisez le décorateur @OneToMany().

user.entity.ts
JS TS

import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}
Astuce Pour en savoir plus sur les relations au sein de TypeORM, visitez le TypeORM documentation.

Chargement automatique des entités#

L'ajout manuel d'entités au tableau entities des options de la source de données peut être fastidieux. En outre, le référencement des entités à partir du module racine ne respecte pas les limites du domaine d'application et provoque des fuites de détails d'implémentation vers d'autres parties de l'application. Pour résoudre ce problème, une solution alternative est fournie. Pour charger automatiquement les entités, définissez la propriété autoLoadEntities de l'objet de configuration (passé dans la méthode forRoot()) à true, comme montré ci-dessous :

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

Si cette option est spécifiée, chaque entité enregistrée par la méthode forFeature() sera automatiquement ajoutée au tableau entities de l'objet de configuration.

Attention Notez que les entités qui ne sont pas enregistrées via la méthode forFeature(), mais qui sont seulement référencées à partir de l'entité (via une relation), ne seront pas incluses par le biais du paramètre autoLoadEntities.

Séparation de la définition de l'entité#

Vous pouvez définir une entité et ses colonnes directement dans le modèle, en utilisant des décorateurs. Mais certaines personnes préfèrent définir les entités et leurs colonnes dans des fichiers séparés en utilisant les " schémas d'entité ".


import { EntitySchema } from 'typeorm';
import { User } from './user.entity';

export const UserSchema = new EntitySchema<User>({
  name: 'User',
  target: User,
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    firstName: {
      type: String,
    },
    lastName: {
      type: String,
    },
    isActive: {
      type: Boolean,
      default: true,
    },
  },
  relations: {
    photos: {
      type: 'one-to-many',
      target: 'Photo', // le nom du PhotoSchema
    },
  },
});
Attention Si vous fournissez l'option target, la valeur de l'option name doit être la même que le nom de la classe cible. Si vous ne fournissez pas de target, vous pouvez utiliser n'importe quel nom.

Nest vous permet d'utiliser une instance de EntitySchema partout où une Entity est attendue, par exemple :


import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([UserSchema])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

Transactions TypeORM#

Une transaction de base de données symbolise une unité de travail effectuée au sein d'un système de gestion de base de données par rapport à une base de données, et traitée de manière cohérente et fiable, indépendamment des autres transactions. Une transaction représente généralement toute modification apportée à une base de données (en savoir plus).

Il existe de nombreuses stratégies différentes pour gérer les transactions TypeORM. Nous recommandons d'utiliser la classe QueryRunner car elle donne un contrôle total sur la transaction.

Tout d'abord, nous devons injecter l'objet DataSource dans une classe de la manière habituelle :


@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
}
Astuce La classe DataSource est importée du package typeorm.

Nous pouvons maintenant utiliser cet objet pour créer une transaction.


async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    // Puisque nous avons des erreurs, revenons sur les changements que nous avons effectués.
    await queryRunner.rollbackTransaction();
  } finally {
    // Vous devez libérer un queryRunner qui a été instancié manuellement
    await queryRunner.release();
  }
}
Astuce Notez que la dataSource n'est utilisée que pour créer le QueryRunner. Cependant, pour tester cette classe, il faudrait simuler l'objet DataSource entier (qui expose plusieurs méthodes). Ainsi, nous recommandons d'utiliser une classe fabrique d'aide (par exemple, QueryRunnerFactory) et de définir une interface avec un ensemble limité de méthodes nécessaires pour maintenir les transactions. Cette technique rend l'utilisation de ces méthodes assez simple.

Vous pouvez également utiliser une approche de type callback avec la méthode transaction de l'objet DataSource (lire la suite).


async createMany(users: User[]) {
  await this.dataSource.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

Abonnés#

Avec les abonnés TypeORM, vous pouvez écouter des événements spécifiques de l'entité.


import {
  DataSource,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { User } from './user.entity';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(dataSource: DataSource) {
    dataSource.subscribers.push(this);
  }

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log(`BEFORE USER INSERTED: `, event.entity);
  }
}
Attention Les abonnés aux événements ne peuvent pas être à portée de requête.

Maintenant, ajoutez la classe UserSubscriber au tableau providers :


import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserSubscriber],
  controllers: [UsersController],
})
export class UsersModule {}
Astuce Apprenez-en plus sur les abonnés de l'entité ici.

Migrations#

Les migrations permettent de mettre à jour de manière incrémentale le schéma de la base de données afin de le maintenir en phase avec le modèle de données de l'application tout en préservant les données existantes dans la base de données. Pour générer, exécuter et inverser les migrations, TypeORM fournit une CLI dédiée.

Les classes de migration sont distinctes du code source de l'application Nest. Leur cycle de vie est géré par le CLI TypeORM. Par conséquent, vous n'êtes pas en mesure de tirer parti de l'injection de dépendance et d'autres fonctionnalités spécifiques à Nest avec les migrations. Pour en savoir plus sur les migrations, suivez le guide dans la documentation TypeORM.

Bases de données multiples#

Certains projets nécessitent des connexions multiples à des bases de données. Ce module permet également d'y parvenir. Pour travailler avec des connexions multiples, il faut d'abord créer les connexions. Dans ce cas, le nom de la source de données devient obligatoire.

Supposons que vous ayez une entité Album stockée dans sa propre base de données.


const defaultOptions = {
  type: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      entities: [User],
    }),
    TypeOrmModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      entities: [Album],
    }),
  ],
})
export class AppModule {}
Remarque Si vous ne définissez pas le name d'une source de données, son nom sera fixé à default. Notez que vous ne devriez pas avoir plusieurs connexions sans nom, ou avec le même nom, sinon elles seront écrasées.
Remarque Si vous utilisez TypeOrmModule.forRootAsync, vous devez également définir le nom de la source de données en dehors de useFactory. Par exemple :

TypeOrmModule.forRootAsync({
  name: 'albumsConnection',
  useFactory: ...,
  inject: ...,
}),

Voir cette issue pour plus de détails.

A ce stade, vous avez les entités User et Album enregistrées avec leur propre source de données. Avec cette configuration, vous devez indiquer à la méthode TypeOrmModule.forFeature() et au décorateur @InjectRepository() quelle source de données doit être utilisée. Si vous ne passez pas de nom de source de données, la source de données default est utilisée.


@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

Vous pouvez également injecter le DataSource ou le EntityManager pour une source de données donnée :


@Injectable()
export class AlbumsService {
  constructor(
    @InjectDataSource('albumsConnection')
    private dataSource: DataSource,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager,
  ) {}
}

Il est également possible d'injecter n'importe quelle DataSource dans les fournisseurs :


@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsConnection: DataSource) => {
        return new AlbumsService(albumsConnection);
      },
      inject: [getDataSourceToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

Tests#

Lorsqu'il s'agit de tester une application de manière unitaire, nous voulons généralement éviter d'établir une connexion à la base de données, afin que nos suites de tests restent indépendantes et que leur processus d'exécution soit aussi rapide que possible. Mais nos classes peuvent dépendre de référentiels qui sont tirés de l'instance de la source de données (connexion). Comment gérer cela ? La solution consiste à créer des référentiels fictifs. Pour ce faire, nous mettons en place des fournisseurs personnalisés. Chaque référentiel enregistré est automatiquement représenté par un jeton <EntityName>Repository, où EntityName est le nom de votre classe d'entité.

Le package @nestjs/typeorm expose la fonction getRepositoryToken() qui retourne un jeton préparé basé sur une entité donnée.


@Module({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository,
    },
  ],
})
export class UsersModule {}

Maintenant un substitut mockRepository sera utilisé comme UsersRepository. Chaque fois qu'une classe demandera le UsersRepository en utilisant un décorateur @InjectRepository(), Nest utilisera l'objet mockRepository enregistré.

Configuration asynchrone#

Vous pouvez vouloir passer les options de votre module de dépôt de manière asynchrone plutôt que statique. Dans ce cas, utilisez la méthode forRootAsync(), qui fournit plusieurs façons de gérer la configuration asynchrone.

Une approche consiste à utiliser une fonction factory :


TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [],
    synchronize: true,
  }),
});

Notre fabrique se comporte comme n'importe quel autre fournisseur asynchrone (par exemple, il peut être async et il est capable d'injecter des dépendances via inject).


TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: true,
  }),
  inject: [ConfigService],
});

Vous pouvez également utiliser la syntaxe useClass :


TypeOrmModule.forRootAsync({
  useClass: TypeOrmConfigService,
});

La construction ci-dessus instanciera TypeOrmConfigService dans TypeOrmModule et l'utilisera pour fournir un objet d'options en appelant createTypeOrmOptions(). Notez que cela signifie que le TypeOrmConfigService doit implémenter l'interface TypeOrmOptionsFactory, comme montré ci-dessous :


@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    };
  }
}

Afin d'éviter la création de TypeOrmConfigService dans TypeOrmModule et d'utiliser un fournisseur importé d'un module différent, vous pouvez utiliser la syntaxe useExisting.


TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

Cette construction fonctionne de la même manière que useClass avec une différence essentielle - TypeOrmModule va chercher dans les modules importés pour réutiliser un ConfigService existant au lieu d'en instancier un nouveau.

Astuce Assurez-vous que la propriété name est définie au même niveau que la propriété useFactory, useClass, ou useValue. Cela permettra à Nest d'enregistrer correctement la source de données sous le jeton d'injection approprié.

Factory de sources de données personnalisées#

En conjonction avec la configuration asynchrone utilisant useFactory, useClass, ou useExisting, vous pouvez optionnellement spécifier une fonction dataSourceFactory qui vous permettra de fournir votre propre source de données TypeORM plutôt que d'autoriser TypeOrmModule à créer la source de données.

dataSourceFactory reçoit les DataSourceOptions TypeORM configurées lors de la configuration asynchrone avec useFactory, useClass, ou useExisting et retourne une Promesse qui résout une DataSource TypeORM.


TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  // Utilisez useFactory, useClass, ou useExisting
  // pour configurer les DataSourceOptions.
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: true,
  }),
  // dataSource reçoit les DataSourceOptions configurées
  // et renvoie une Promise<DataSource>.
  dataSourceFactory: async (options) => {
    const dataSource = await new DataSource(options).initialize();
    return dataSource;
  },
});
Astuce La classe DataSource est importée du package typeorm.

Exemple#

Un exemple concret est disponible ici.

Intégration Sequelize

Une alternative à l'utilisation de TypeORM est d'utiliser l'ORM Sequelize avec le package @nestjs/sequelize. De plus, nous nous appuyons sur le package sequelize-typescript qui fournit un ensemble de décorateurs supplémentaires pour définir les entités de manière déclarative.

Pour commencer à l'utiliser, nous devons d'abord installer les dépendances nécessaires. Dans ce chapitre, nous utiliserons le populaire SGBD relationnel MySQL, mais Sequelize prend en charge de nombreuses bases de données relationnelles, telles que PostgreSQL, MySQL, Microsoft SQL Server, SQLite et MariaDB. La procédure décrite dans ce chapitre est la même pour toutes les bases de données prises en charge par Sequelize. Vous devrez simplement installer les bibliothèques API client associées à la base de données que vous avez sélectionnée.


$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize

Une fois le processus d'installation terminé, nous pouvons importer le module SequelizeModule dans le module racine AppModule.

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    }),
  ],
})
export class AppModule {}

La méthode forRoot() supporte toutes les propriétés de configuration exposées par le constructeur Sequelize (lire la suite). En outre, il existe plusieurs propriétés de configuration supplémentaires décrites ci-dessous.

retryAttemptsNombre de tentatives de connexion à la base de données (par défaut : 10)
retryDelayDélai entre les tentatives de reconnexion (ms) (par défaut : 3000)
autoLoadModelsSi true, les modèles seront chargés automatiquement (par défaut : false)
keepConnectionAliveSi true, la connexion ne sera pas fermée lors de l'arrêt de l'application (par défaut : false)
synchronizeSi true, les modèles chargés automatiquement seront synchronisés (par défaut : true)

Une fois cela fait, l'objet Sequelize sera disponible pour être injecté dans l'ensemble du projet (sans avoir besoin d'importer des modules), par exemple :

app.service.ts
JS TS

import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(private sequelize: Sequelize) {}
}

import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Dependencies(Sequelize)
@Injectable()
export class AppService {
  constructor(sequelize) {
    this.sequelize = sequelize;
  }
}

Modèles#

Sequelize met en œuvre le modèle Active Record. Avec ce modèle, vous utilisez directement les classes de modèle pour interagir avec la base de données. Pour continuer l'exemple, nous avons besoin d'au moins un modèle. Définissons le modèle User.

user.model.ts
JS TS

import { Column, Model, Table } from 'sequelize-typescript';

@Table
export class User extends Model {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;
}
Astuce Apprenez-en plus sur les décorateurs disponibles ici.

Le fichier modèle User se trouve dans le répertoire users. Ce répertoire contient tous les fichiers relatifs au module Users. Vous pouvez décider de l'emplacement de vos fichiers de modèle, cependant, nous recommandons de les créer près de leur domaine, dans le répertoire du module correspondant.

Pour commencer à utiliser le modèle User, nous devons le faire connaître à Sequelize en l'insérant dans le tableau models dans les options de la méthode forRoot() du module :

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [User],
    }),
  ],
})
export class AppModule {}

Ensuite, regardons le module UsersModule :

users.module.ts
JS TS

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

Ce module utilise la méthode forFeature() pour définir quels modèles sont enregistrés dans la portée courante. Avec cela en place, nous pouvons injecter le UserModel dans le UsersService en utilisant le décorateur @InjectModel() :

users.service.ts
JS TS

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userModel.findAll();
  }

  findOne(id: string): Promise<User> {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    await user.destroy();
  }
}

import { Injectable, Dependencies } from '@nestjs/common';
import { getModelToken } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
@Dependencies(getModelToken(User))
export class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  async findAll() {
    return this.userModel.findAll();
  }

  findOne(id) {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id) {
    const user = await this.findOne(id);
    await user.destroy();
  }
}
Remarque N'oubliez pas d'importer le module UsersModule dans le module racine AppModule.

Si vous voulez utiliser le répertoire en dehors du module qui importe SequelizeModule.forFeature, vous devrez réexporter les fournisseurs générés par ce module. Vous pouvez le faire en exportant le module entier, comme ceci :

users.module.ts
JS TS

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  exports: [SequelizeModule]
})
export class UsersModule {}

Maintenant, si nous importons UsersModule dans UserHttpModule, nous pouvons utiliser @InjectModel(User) dans les fournisseurs de ce dernier module.

users-http.module.ts
JS TS

import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

Relations#

Les relations sont des associations établies entre deux ou plusieurs tables. Les relations sont basées sur des champs communs à chaque table, impliquant souvent des clés primaires et étrangères.

Il existe trois types de relations :

One-to-oneChaque ligne de la table primaire a une et une seule ligne associée dans la table étrangère.
One-to-many / Many-to-oneChaque ligne de la table primaire a une ou plusieurs lignes apparentées dans la table étrangère.
Many-to-manyChaque ligne de la table primaire a plusieurs lignes apparentées dans la table étrangère, et chaque enregistrement de la table étrangère a plusieurs lignes apparentées dans la table primaire.

Pour définir des relations entre les modèles, utilisez les décorateurs correspondants. Par exemple, pour définir que chaque User peut avoir plusieurs photos, utilisez le décorateur @HasMany().

user.model.ts
JS TS

import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';

@Table
export class User extends Model {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;

  @HasMany(() => Photo)
  photos: Photo[];
}
Astuce Pour en savoir plus sur les associations dans Sequelize, lisez ce chapitre.

Chargement automatique des modèles#

Ajouter manuellement des modèles au tableau models des options de connexion peut être fastidieux. De plus, référencer des modèles à partir du module racine brise les frontières du domaine d'application et provoque des fuites de détails d'implémentation vers d'autres parties de l'application. Pour résoudre ce problème, chargez automatiquement les modèles en définissant les propriétés autoLoadModels et synchronize de l'objet de configuration (passé dans la méthode forRoot()) à true, comme montré ci-dessous :

app.module.ts
JS TS

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...
      autoLoadModels: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Si cette option est spécifiée, chaque modèle enregistré par la méthode forFeature() sera automatiquement ajouté au tableau models de l'objet de configuration.

Attention Notez que les modèles qui ne sont pas enregistrés via la méthode forFeature(), mais qui sont seulement référencés à partir du modèle (via une association), ne seront pas inclus.

Transactions Sequelize#

Une transaction de base de données symbolise une unité de travail effectuée au sein d'un système de gestion de base de données par rapport à une base de données, et traitée de manière cohérente et fiable, indépendamment des autres transactions. Une transaction représente généralement toute modification apportée à une base de données (en savoir plus).

Il existe de nombreuses stratégies différentes pour gérer les transactions Sequelize. Vous trouverez ci-dessous un exemple de mise en œuvre d'une transaction gérée (auto-callback).

Tout d'abord, nous devons injecter l'objet Sequelize dans une classe de la manière habituelle :


@Injectable()
export class UsersService {
  constructor(private sequelize: Sequelize) {}
}
Astuce La classe Sequelize est importée du package sequelize-typescript.

Nous pouvons maintenant utiliser cet objet pour créer une transaction.


async createMany() {
  try {
    await this.sequelize.transaction(async t => {
      const transactionHost = { transaction: t };

      await this.userModel.create(
          { firstName: 'Abraham', lastName: 'Lincoln' },
          transactionHost,
      );
      await this.userModel.create(
          { firstName: 'John', lastName: 'Boothe' },
          transactionHost,
      );
    });
  } catch (err) {
    // La transaction a été annulée
    // err est le rejet de la chaîne de promesses renvoyée au callback de la transaction
  }
}
Astuce Notez que l'instance Sequelize n'est utilisée que pour démarrer la transaction. Cependant, pour tester cette classe, il faudrait simuler l'objet Sequelize entier (qui expose plusieurs méthodes). Nous recommandons donc d'utiliser une classe factory d'aide (par exemple, TransactionRunner) et de définir une interface avec un ensemble limité de méthodes nécessaires pour maintenir les transactions. Cette technique rend l'utilisation de ces méthodes assez simple.

Migrations#

Les Migrations permettent de mettre à jour de manière incrémentale le schéma de la base de données afin de le maintenir en phase avec le modèle de données de l'application tout en préservant les données existantes dans la base de données. Pour générer, exécuter et inverser les migrations, Sequelize fournit une CLI dédié.

Les classes de migration sont distinctes du code source de l'application Nest. Leur cycle de vie est géré par l'interface de programmation Sequelize. Par conséquent, vous n'êtes pas en mesure de tirer parti de l'injection de dépendances et d'autres fonctionnalités spécifiques à Nest avec les migrations. Pour en savoir plus sur les migrations, suivez le guide dans la documentation Sequelize.

Bases de données multiples#

Certains projets nécessitent des connexions multiples à des bases de données. Ce module permet également d'y parvenir. Pour travailler avec plusieurs connexions, il faut d'abord créer les connexions. Dans ce cas, le nom de la connexion devient obligatoire.

Supposons que vous ayez une entité Album stockée dans sa propre base de données.


const defaultOptions = {
  dialect: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      models: [User],
    }),
    SequelizeModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      models: [Album],
    }),
  ],
})
export class AppModule {}
Remarque Si vous ne définissez pas le name d'une connexion, son nom sera fixé à default. Notez que vous ne devriez pas avoir plusieurs connexions sans nom, ou avec le même nom, sinon elles seront écrasées.

A ce stade, vous avez les modèles User et Album enregistrés avec leur propre connexion. Avec cette configuration, vous devez indiquer à la méthode SequelizeModule.forFeature() et au décorateur @InjectModel() quelle connexion doit être utilisée. Si vous ne passez pas de nom de connexion, la connexion default est utilisée.


@Module({
  imports: [
    SequelizeModule.forFeature([User]),
    SequelizeModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

Vous pouvez également injecter l'instance Sequelize pour une connexion donnée :


@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private sequelize: Sequelize,
  ) {}
}

Il est également possible d'injecter n'importe quelle instance de Sequelize dans les fournisseurs :


@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsSequelize: Sequelize) => {
        return new AlbumsService(albumsSequelize);
      },
      inject: [getDataSourceToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

Tests#

Lorsqu'il s'agit de tester une application de manière unitaire, nous voulons généralement éviter d'établir une connexion à la base de données, afin que nos suites de tests restent indépendantes et que leur processus d'exécution soit aussi rapide que possible. Mais nos classes peuvent dépendre de modèles qui sont tirés de l'instance de connexion. Comment gérer cela ? La solution consiste à créer des modèles fictifs. Pour ce faire, nous mettons en place des fournisseurs personnalisés. Chaque modèle enregistré est automatiquement représenté par un jeton <ModelName>Model, où ModelName est le nom de votre classe de modèle.

Le package @nestjs/sequelize expose la fonction getModelToken() qui retourne un jeton préparé basé sur un modèle donné.


@Module({
  providers: [
    UsersService,
    {
      provide: getModelToken(User),
      useValue: mockModel,
    },
  ],
})
export class UsersModule {}

Maintenant, un substitut mockModel sera utilisé comme UserModel. Chaque fois qu'une classe demandera le ModèleUtilisateur en utilisant un décorateur @InjectModel(), Nest utilisera l'objet mockModel enregistré.

Configuration asynchrone#

Vous pouvez vouloir passer vos options SequelizeModule de manière asynchrone plutôt que statique. Dans ce cas, utilisez la méthode forRootAsync(), qui fournit plusieurs façons de gérer la configuration asynchrone.

Une approche consiste à utiliser une fonction d'usine :


SequelizeModule.forRootAsync({
  useFactory: () => ({
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    models: [],
  }),
});

Notre factory se comporte comme n'importe quel autre fournisseur asynchrone (par exemple, il peut être async et il est capable d'injecter des dépendances via inject).


SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    dialect: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    models: [],
  }),
  inject: [ConfigService],
});

Vous pouvez également utiliser la syntaxe useClass :


SequelizeModule.forRootAsync({
  useClass: SequelizeConfigService,
});

La construction ci-dessus instanciera SequelizeConfigService dans SequelizeModule et l'utilisera pour fournir un objet d'options en appelant createSequelizeOptions(). Notez que cela signifie que le SequelizeConfigService doit implémenter l'interface SequelizeOptionsFactory, comme montré ci-dessous :


@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
  createSequelizeOptions(): SequelizeModuleOptions {
    return {
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    };
  }
}

Afin d'éviter la création de SequelizeConfigService dans SequelizeModule et d'utiliser un fournisseur importé d'un module différent, vous pouvez utiliser la syntaxe useExisting.


SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

Cette construction fonctionne de la même manière que useClass avec une différence essentielle - SequelizeModule va chercher dans les modules importés pour réutiliser un ConfigService existant au lieu d'en instancier un nouveau.

Exemple#

Un exemple concret est disponible ici.

Soutenez-nous

Nest est un projet open source sous licence MIT. Il peut se développer grâce au soutien de ces personnes formidables. Si vous souhaitez les rejoindre, apprenez-en plus ici.

Sponsors Principaux

Trilon LogoMarblism LogoMojam LogoAmplication Logo

Sponsors / Partenaires

Devenir un sponsor