NestJS 數(shù)據(jù)庫(kù)

2023-09-08 16:17 更新

Nest 與數(shù)據(jù)庫(kù)無(wú)關(guān),允許您輕松地與任何 SQL 或 NoSQL 數(shù)據(jù)庫(kù)集成。根據(jù)您的偏好,您有許多可用的選項(xiàng)。一般來(lái)說(shuō),將 Nest 連接到數(shù)據(jù)庫(kù)只需為數(shù)據(jù)庫(kù)加載一個(gè)適當(dāng)?shù)?nbsp;Node.js 驅(qū)動(dòng)程序,就像使用 Express 或 Fastify 一樣。

您還可以直接使用任何通用的 Node.js 數(shù)據(jù)庫(kù)集成庫(kù)或 ORM ,例如 Sequelize (recipe)、knexjs (tutorial)`和 TypeORM ,以在更高的抽象級(jí)別上進(jìn)行操作。

為了方便起見(jiàn),Nest 還提供了與現(xiàn)成的 TypeORM 與 @nestjs/typeorm 的緊密集成,我們將在本章中對(duì)此進(jìn)行介紹,而與 @nestjs/mongoose 的緊密集成將在這一章中介紹。這些集成提供了附加的特定于 nestjs 的特性,比如模型/存儲(chǔ)庫(kù)注入、可測(cè)試性和異步配置,從而使訪問(wèn)您選擇的數(shù)據(jù)庫(kù)更加容易。

TypeORM 集成

為了與 SQL和 NoSQL 數(shù)據(jù)庫(kù)集成,Nest 提供了 @nestjs/typeorm 包。Nest 使用TypeORM是因?yàn)樗?nbsp;TypeScript 中最成熟的對(duì)象關(guān)系映射器( ORM )。因?yàn)樗怯?nbsp;TypeScript 編寫的,所以可以很好地與 Nest 框架集成。

為了開始使用它,我們首先安裝所需的依賴項(xiàng)。在本章中,我們將演示如何使用流行的 Mysql , TypeORM 提供了對(duì)許多關(guān)系數(shù)據(jù)庫(kù)的支持,比如 PostgreSQL 、Oracle、Microsoft SQL Server、SQLite,甚至像 MongoDB這樣的 NoSQL 數(shù)據(jù)庫(kù)。我們?cè)诒菊轮薪榻B的過(guò)程對(duì)于 TypeORM 支持的任何數(shù)據(jù)庫(kù)都是相同的。您只需為所選數(shù)據(jù)庫(kù)安裝相關(guān)的客戶端 API 庫(kù)。

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

安裝過(guò)程完成后,我們可以將 TypeOrmModule 導(dǎo)入AppModule 。

app.module.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 {}

forRoot() 方法支持所有TypeORM包中createConnection()函數(shù)暴露出的配置屬性。其他一些額外的配置參數(shù)描述如下:

參數(shù)說(shuō)明
retryAttempts重試連接數(shù)據(jù)庫(kù)的次數(shù)(默認(rèn):10)
retryDelay兩次重試連接的間隔(ms)(默認(rèn):3000)
autoLoadEntities如果為true,將自動(dòng)加載實(shí)體(默認(rèn):false)
keepConnectionAlive如果為true,在應(yīng)用程序關(guān)閉后連接不會(huì)關(guān)閉(默認(rèn):false)

更多連接選項(xiàng)見(jiàn)這里

另外,我們可以創(chuàng)建 ormconfig.json ,而不是將配置對(duì)象傳遞給 forRoot()。

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "root",
  "database": "test",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}

然后,我們可以不帶任何選項(xiàng)地調(diào)用 forRoot() :

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

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

靜態(tài)全局路徑(例如 dist/**/*.entity{ .ts,.js} )不適用于 Webpack 熱重載。

注意,ormconfig.json 文件由typeorm庫(kù)載入,因此,任何上述參數(shù)之外的屬性都不會(huì)被應(yīng)用(例如由forRoot()方法內(nèi)部支持的屬性–例如autoLoadEntities和retryDelay())

一旦完成,TypeORM 的Connection和 EntityManager 對(duì)象就可以在整個(gè)項(xiàng)目中注入(不需要導(dǎo)入任何模塊),例如:

app.module.ts
import { Connection } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), PhotoModule],
})
export class AppModule {
  constructor(private readonly connection: Connection) {}
}

存儲(chǔ)庫(kù)模式

TypeORM 支持存儲(chǔ)庫(kù)設(shè)計(jì)模式,因此每個(gè)實(shí)體都有自己的存儲(chǔ)庫(kù)??梢詮臄?shù)據(jù)庫(kù)連接獲得這些存儲(chǔ)庫(kù)。

為了繼續(xù)這個(gè)示例,我們需要至少一個(gè)實(shí)體。我們來(lái)定義User 實(shí)體。

user.entity.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;
}

關(guān)于實(shí)體的更多內(nèi)容見(jiàn)TypeORM 文檔。

該 User 實(shí)體在 users 目錄下。這個(gè)目錄包含了和 UsersModule模塊有關(guān)的所有文件。你可以決定在哪里保存模型文件,但我們推薦在他們的域中就近創(chuàng)建,即在相應(yīng)的模塊目錄中。

要開始使用 user 實(shí)體,我們需要在模塊的forRoot()方法的選項(xiàng)中(除非你使用一個(gè)靜態(tài)的全局路徑)將它插入entities數(shù)組中來(lái)讓 TypeORM知道它的存在。

app.module.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 {}

現(xiàn)在讓我們看一下 UsersModule:

user.module.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 {}

此模塊使用 forFeature() 方法定義在當(dāng)前范圍中注冊(cè)哪些存儲(chǔ)庫(kù)。這樣,我們就可以使用 @InjectRepository()裝飾器將 UsersRepository 注入到 UsersService 中:

users.service.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: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

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

不要忘記將 UsersModule 導(dǎo)入根 AppModule。

如果要在導(dǎo)入TypeOrmModule.forFeature 的模塊之外使用存儲(chǔ)庫(kù),則需要重新導(dǎo)出由其生成的提供程序。 您可以通過(guò)導(dǎo)出整個(gè)模塊來(lái)做到這一點(diǎn),如下所示:

users.module.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 {}

現(xiàn)在,如果我們?cè)?nbsp;UserHttpModule 中導(dǎo)入 UsersModule ,我們可以在后一個(gè)模塊的提供者中使用 @InjectRepository(User)。

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

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

關(guān)系

關(guān)系是指兩個(gè)或多個(gè)表之間的聯(lián)系。關(guān)系基于每個(gè)表中的常規(guī)字段,通常包含主鍵和外鍵。

關(guān)系有三種:

名稱說(shuō)明
一對(duì)一主表中的每一行在外部表中有且僅有一個(gè)對(duì)應(yīng)行。使用@OneToOne()裝飾器來(lái)定義這種類型的關(guān)系
一對(duì)多/多對(duì)一主表中的每一行在外部表中有一個(gè)或多的對(duì)應(yīng)行。使用@OneToMany()@ManyToOne()裝飾器來(lái)定義這種類型的關(guān)系
多對(duì)多主表中的每一行在外部表中有多個(gè)對(duì)應(yīng)行,外部表中的每個(gè)記錄在主表中也有多個(gè)行。使用@ManyToMany()裝飾器來(lái)定義這種類型的關(guān)系

使用對(duì)應(yīng)的裝飾器來(lái)定義實(shí)體的關(guān)系。例如,要定義每個(gè)User可以有多個(gè)Photo,可以使用@OneToMany()裝飾器。

user.entity.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[];
}

要了解 TypeORM 中關(guān)系的內(nèi)容,可以查看TypeORM 文檔

自動(dòng)載入實(shí)體

手動(dòng)將實(shí)體一一添加到連接選項(xiàng)的entities數(shù)組中的工作會(huì)很無(wú)聊。此外,在根模塊中涉及實(shí)體破壞了應(yīng)用的域邊界,并可能將應(yīng)用的細(xì)節(jié)泄露給應(yīng)用的其他部分。針對(duì)這一情況,可以使用靜態(tài)全局路徑(例如, dist/*/.entity{.ts,.js})。

注意,webpack不支持全局路徑,因此如果你要在單一倉(cāng)庫(kù)(Monorepo)中構(gòu)建應(yīng)用,可能不能使用全局路徑。針對(duì)這一問(wèn)題,有另外一個(gè)可選的方案。在配置對(duì)象的屬性中(傳遞給forRoot()方法的)設(shè)置autoLoadEntities屬性為true來(lái)自動(dòng)載入實(shí)體,示意如下:

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

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

通過(guò)配置這一選項(xiàng),每個(gè)通過(guò)forFeature()注冊(cè)的實(shí)體都會(huì)自動(dòng)添加到配置對(duì)象的entities數(shù)組中。

注意,那些沒(méi)有通過(guò)forFeature()方法注冊(cè),而僅僅是在實(shí)體中被引用(通過(guò)關(guān)系)的實(shí)體不能通過(guò)autoLoadEntities配置被包含。

事務(wù)

數(shù)據(jù)庫(kù)事務(wù)代表在數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)中針對(duì)數(shù)據(jù)庫(kù)的一組操作,這組操作是有關(guān)的、可靠的并且和其他事務(wù)相互獨(dú)立的。一個(gè)事務(wù)通??梢源頂?shù)據(jù)庫(kù)中的任何變更(了解更多)。

TypeORM 事務(wù)中有很多不同策略來(lái)處理事務(wù),我們推薦使用QueryRunner類,因?yàn)樗鼘?duì)事務(wù)是完全可控的。

首先,我們需要將Connection對(duì)象以正常方式注入:

@Injectable()
export class UsersService {
  constructor(private connection: Connection) {}
}

Connection類需要從typeorm包中導(dǎo)入

現(xiàn)在,我們可以使用這個(gè)對(duì)象來(lái)創(chuàng)建一個(gè)事務(wù)。

async createMany(users: User[]) {
  const queryRunner = this.connection.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) {
    //如果遇到錯(cuò)誤,可以回滾事務(wù)
    await queryRunner.rollbackTransaction();
  } finally {
    //你需要手動(dòng)實(shí)例化并部署一個(gè)queryRunner
    await queryRunner.release();
  }
}

注意connection僅用于創(chuàng)建QueryRunner。然而,要測(cè)試這個(gè)類,就需要模擬整個(gè)Connection對(duì)象(它暴露出來(lái)的幾個(gè)方法),因此,我們推薦采用一個(gè)幫助工廠類(也就是QueryRunnerFactory)并且定義一個(gè)包含僅限于維持事務(wù)需要的方法的接口。這一技術(shù)讓模擬這些方法變得非常直接。

可選地,你可以使用一個(gè)Connection對(duì)象的回調(diào)函數(shù)風(fēng)格的transaction方法(閱讀更多)。

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

不推薦使用裝飾器來(lái)控制事務(wù)(@Transaction()和@TransactionManager())。

訂閱者

使用 TypeORM訂閱者,你可以監(jiān)聽(tīng)特定的實(shí)體事件。

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

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

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log(`BEFORE USER INSERTED: `, event.entity);
  }
}

事件訂閱者不能是請(qǐng)求范圍的。

現(xiàn)在,將UserSubscriber類添加到providers數(shù)組。

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 {}

更多實(shí)體訂閱者內(nèi)容見(jiàn)這里。

遷移

遷移提供了一個(gè)在保存數(shù)據(jù)庫(kù)中現(xiàn)有數(shù)據(jù)的同時(shí)增量升級(jí)數(shù)據(jù)庫(kù)使其與應(yīng)用中的數(shù)據(jù)模型保持同步的方法。TypeORM 提供了一個(gè)專用CLI 命令行工具用于生成、運(yùn)行以及回滾遷移。

遷移類和Nest應(yīng)用源碼是分開的。他們的生命周期由TypeORM CLI管理,因此,你不能在遷移中使用依賴注入和其他Nest專有特性。在TypeORM 文檔 中查看更多關(guān)于遷移的內(nèi)容。

多個(gè)數(shù)據(jù)庫(kù)

某些項(xiàng)目可能需要多個(gè)數(shù)據(jù)庫(kù)連接。這也可以通過(guò)本模塊實(shí)現(xiàn)。要使用多個(gè)連接,首先要做的是創(chuàng)建這些連接。在這種情況下,連接命名成為必填項(xiàng)。

假設(shè)你有一個(gè)Album 實(shí)體存儲(chǔ)在他們自己的數(shù)據(jù)庫(kù)中。

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 {}

如果未為連接設(shè)置任何 name ,則該連接的名稱將設(shè)置為 default。請(qǐng)注意,不應(yīng)該有多個(gè)沒(méi)有名稱或同名的連接,否則它們會(huì)被覆蓋。

此時(shí),您的User 和 Album 實(shí)體中的每一個(gè)都已在各自的連接中注冊(cè)。通過(guò)此設(shè)置,您必須告訴 TypeOrmModule.forFeature() 方法和 @InjectRepository() 裝飾器應(yīng)該使用哪種連接。如果不傳遞任何連接名稱,則使用 default 連接。

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

您也可以為給定的連接注入 Connection 或 EntityManager:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private connection: Connection,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager
  ) {}
}

測(cè)試

在單元測(cè)試我們的應(yīng)用程序時(shí),我們通常希望避免任何數(shù)據(jù)庫(kù)連接,從而使我們的測(cè)試適合于獨(dú)立,并使它們的執(zhí)行過(guò)程盡可能快。但是我們的類可能依賴于從連接實(shí)例中提取的存儲(chǔ)庫(kù)。那是什么?解決方案是創(chuàng)建假存儲(chǔ)庫(kù)。為了實(shí)現(xiàn)這一點(diǎn),我們?cè)O(shè)置了[自定義提供者]。事實(shí)上,每個(gè)注冊(cè)的存儲(chǔ)庫(kù)都由 entitynamereposition 標(biāo)記表示,其中 EntityName 是實(shí)體類的名稱。

@nestjs/typeorm 包提供了基于給定實(shí)體返回準(zhǔn)備好 token 的 getRepositoryToken() 函數(shù)。

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

現(xiàn)在, 將使用mockRepository 作為 UsersRepository。每當(dāng)任何提供程序使用 @InjectRepository() 裝飾器請(qǐng)求 UsersRepository 時(shí), Nest 會(huì)使用注冊(cè)的 mockRepository 對(duì)象。

定制存儲(chǔ)庫(kù)

TypeORM 提供稱為自定義存儲(chǔ)庫(kù)的功能。要了解有關(guān)它的更多信息,請(qǐng)?jiān)L問(wèn)此頁(yè)面?;旧希远x存儲(chǔ)庫(kù)允許您擴(kuò)展基本存儲(chǔ)庫(kù)類,并使用幾種特殊方法對(duì)其進(jìn)行豐富。

要?jiǎng)?chuàng)建自定義存儲(chǔ)庫(kù),請(qǐng)使用 @EntityRepository() 裝飾器和擴(kuò)展 Repository 類。

@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}

@EntityRepository() 和 Repository 來(lái)自 typeorm 包。

創(chuàng)建類后,下一步是將實(shí)例化責(zé)任移交給 Nest。為此,我們必須將 AuthorRepository 類傳遞給 TypeOrm.forFeature() 函數(shù)。

@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}

之后,只需使用以下構(gòu)造注入存儲(chǔ)庫(kù):

@Injectable()
export class AuthorService {
  constructor(private readonly authorRepository: AuthorRepository) {}
}

異步配置

通常,您可能希望異步傳遞模塊選項(xiàng),而不是事先傳遞它們。在這種情況下,使用 forRootAsync() 函數(shù),提供了幾種處理異步數(shù)據(jù)的方法。

第一種可能的方法是使用工廠函數(shù):

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

我們的工廠的行為與任何其他異步提供者一樣(例如,它可以是異步的,并且它能夠通過(guò)inject注入依賴)。

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

或者,您可以使用useClass語(yǔ)法。

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

上面的構(gòu)造將 TypeOrmConfigService 在內(nèi)部進(jìn)行實(shí)例化 TypeOrmModule,并將利用它來(lái)創(chuàng)建選項(xiàng)對(duì)象。在 TypeOrmConfigService 必須實(shí)現(xiàn) TypeOrmOptionsFactory 的接口。

上面的構(gòu)造將在TypeOrmModule內(nèi)部實(shí)例化TypeOrmConfigService,并通過(guò)調(diào)用createTypeOrmOptions()

@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    };
  }
}

為了防止在 TypeOrmModule 中創(chuàng)建 TypeOrmConfigService 并使用從不同模塊導(dǎo)入的提供程序,可以使用 useExisting 語(yǔ)法。

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

這個(gè)構(gòu)造與 useClass 的工作原理相同,但有一個(gè)關(guān)鍵的區(qū)別 — TypeOrmModule 將查找導(dǎo)入的模塊來(lái)重用現(xiàn)有的 ConfigService,而不是實(shí)例化一個(gè)新的 ConfigService。

示例

這兒有一個(gè)可用的例子。

Sequelize 集成

另一個(gè)使用TypeORM的選擇是使用@nestjs/sequelize包中的Sequelize ROM。額外地,我們使用sequelize-typescript包來(lái)提供一系列額外的裝飾器以聲明實(shí)體。

要開始使用它,我們首先安裝需要的依賴。在本章中,我們通過(guò)流行的MySQL關(guān)系數(shù)據(jù)庫(kù)來(lái)進(jìn)行說(shuō)明。Sequelize支持很多種關(guān)系數(shù)據(jù)庫(kù),例如PostgreSQL,MySQL,Microsoft SQL Server,SQLite以及MariaDB。本章中的步驟也適合其他任何Sequelize支持的數(shù)據(jù)庫(kù)。你只要簡(jiǎn)單地安裝所選數(shù)據(jù)庫(kù)相應(yīng)的客戶端 API 庫(kù)就可以。

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

安裝完成后,就可以將SequelizeModule導(dǎo)入到根AppModule中。

app.module.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 {}

forRoot()方法支持所有Sequelize構(gòu)造器(了解更多)暴露的配置屬性。下面是一些額外的配置屬性。

名稱說(shuō)明
retryAttempts嘗試連接數(shù)據(jù)庫(kù)的次數(shù)(默認(rèn):10)
retryDelay兩次連接之間間隔時(shí)間(ms)(默認(rèn):3000)
autoLoadModels如果為true,模型將自動(dòng)載入(默認(rèn):false)
keepConnectionAlive如果為true,在應(yīng)用關(guān)閉后連接將不會(huì)關(guān)閉(默認(rèn):false)
synchronize如果為true,自動(dòng)載入的模型將同步(默認(rèn):false)

一旦這些完成了,Sequelize對(duì)象就可以注入到整個(gè)項(xiàng)目中(不需要在任何模塊中再引入),例如:

app.service.ts
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

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

模型

Sequelize采用活動(dòng)記錄(Active Record)模式,在這一模式下,你可以使用模型類直接和數(shù)據(jù)庫(kù)交互。要繼續(xù)該示例,我們至少需要一個(gè)模型,讓我們定義這個(gè)User模型:

user.model.ts
import { Column, Model, Table } from 'sequelize-typescript';

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

  @Column
  lastName: string;

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

查看更多的可用裝飾器。

User模型文件在users目錄下。該目錄包含了和UsersModule有關(guān)的所有文件。你可以決定在哪里保存模型文件,但我們推薦在他們的域中就近創(chuàng)建,即在相應(yīng)的模塊目錄中。

要開始使用User模型,我們需要通過(guò)將其插入到forRoot()方法選項(xiàng)的models數(shù)組中來(lái)讓Sequelize知道它的存在。

app.module.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 {}

接下來(lái)我們看看UsersModule:

users.module.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 {}

這個(gè)模塊使用forFeature()方法來(lái)定義哪個(gè)模型被注冊(cè)在當(dāng)前范圍中。我們可以使用@InjectModel()裝飾器來(lái)把UserModel注入到UsersService中。

users.service.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();
  }
}

不要忘記在根AppModule中導(dǎo)入U(xiǎn)sersModule。

如果你要在導(dǎo)入SequelizeModule.forFreature的模塊之外使用存儲(chǔ)庫(kù),你需要重新導(dǎo)出其生成的提供者。你可以像這樣將整個(gè)模塊導(dǎo)出:

users.module.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 {}

現(xiàn)在如果我們?cè)赨serHttpModule中引入U(xiǎn)sersModule,我們可以在后一個(gè)模塊的提供者中使用@InjectModel(User)。

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

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

關(guān)系

關(guān)系是指兩個(gè)或多個(gè)表之間的聯(lián)系。關(guān)系基于每個(gè)表中的常規(guī)字段,通常包含主鍵和外鍵。

關(guān)系有三種:

名稱說(shuō)明
一對(duì)一主表中的每一行在外部表中有且僅有一個(gè)對(duì)應(yīng)行。使用@OneToOne()裝飾器來(lái)定義這種類型的關(guān)系
一對(duì)多/多對(duì)一主表中的每一行在外部表中有一個(gè)或多的對(duì)應(yīng)行。使用@OneToMany()@ManyToOne()裝飾器來(lái)定義這種類型的關(guān)系
多對(duì)多主表中的每一行在外部表中有多個(gè)對(duì)應(yīng)行,外部表中的每個(gè)記錄在主表中也有多個(gè)行。使用@ManyToMany()裝飾器來(lái)定義這種類型的關(guān)系

使用對(duì)應(yīng)的裝飾器來(lái)定義實(shí)體的關(guān)系。例如,要定義每個(gè)User可以有多個(gè)Photo,可以使用@HasMany()裝飾器。

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

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

  @Column
  lastName: string;

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

  @HasMany(() => Photo)
  photos: Photo[];
}

閱讀本章了解更多關(guān)于Sequelize的內(nèi)容。

自動(dòng)載入模型

手動(dòng)將模型一一添加到連接選項(xiàng)的models數(shù)組中的工作會(huì)很無(wú)聊。此外,在根模塊中涉及實(shí)體破壞了應(yīng)用的域邊界,并可能將應(yīng)用的細(xì)節(jié)泄露給應(yīng)用的其他部分。針對(duì)這一情況,在配置對(duì)象的屬性中(傳遞給forRoot()方法的)設(shè)置autoLoadModels和synchronize屬性來(lái)自動(dòng)載入模型,示意如下:

app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

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

通過(guò)配置這一選項(xiàng),每個(gè)通過(guò)forFeature()注冊(cè)的實(shí)體都會(huì)自動(dòng)添加到配置對(duì)象的models數(shù)組中。

注意,這不包含那些沒(méi)有通過(guò)forFeature()方法注冊(cè),而僅僅是在實(shí)體中被引用(通過(guò)關(guān)系)的模型。

事務(wù)

數(shù)據(jù)庫(kù)事務(wù)代表在數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)中針對(duì)數(shù)據(jù)庫(kù)的一組操作,這組操作是有關(guān)的、可靠的并且和其他事務(wù)相互獨(dú)立的。一個(gè)事務(wù)通??梢源頂?shù)據(jù)庫(kù)中的任何變更(了解更多)。

Sequelize事務(wù)中有很多不同策略來(lái)處理事務(wù),下面是一個(gè)管理事務(wù)的示例(自動(dòng)回調(diào))。

首先,我們需要將Sequelize對(duì)象以正常方式注入:

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

Sequelize類需要從sequelize-typescript包中導(dǎo)入

現(xiàn)在,我們可以使用這個(gè)對(duì)象來(lái)創(chuàng)建一個(gè)事務(wù)。

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) {
    // 一旦發(fā)生錯(cuò)誤,事務(wù)會(huì)回滾
  }
}

注意Sequelize僅用于開始一個(gè)事務(wù)。然而,要測(cè)試這個(gè)類,就需要模擬整個(gè)Sequelize對(duì)象(它暴露出來(lái)的幾個(gè)方法),因此,我們推薦采用一個(gè)幫助工廠類(也就是TransactionRunner)并且定義一個(gè)包含僅限于維持事務(wù)需要的方法的接口。這一技術(shù)讓模擬這些方法變得非常直接。

可選地,你可以使用一個(gè)Connection對(duì)象的回調(diào)函數(shù)風(fēng)格的transaction方法(閱讀更多)。

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

不推薦使用裝飾器來(lái)控制事務(wù)(@Transaction()和@TransactionManager())。

遷移

遷移提供了一個(gè)在保存數(shù)據(jù)庫(kù)中現(xiàn)有數(shù)據(jù)的同時(shí)增量升級(jí)數(shù)據(jù)庫(kù)使其與應(yīng)用中的數(shù)據(jù)模型保持同步的方法。Sequelize提供了一個(gè)專用CLI 命令行工具用于生成、運(yùn)行以及回滾遷移。

遷移類和Nest應(yīng)用源碼是分開的。他們的生命周期由TypeORM CLI管理,因此,你不能在遷移中使用依賴注入和其他Nest專有特性。在Sequelize文檔 中查看更多關(guān)于遷移的內(nèi)容。

多個(gè)數(shù)據(jù)庫(kù)

某些項(xiàng)目可能需要多個(gè)數(shù)據(jù)庫(kù)連接。這也可以通過(guò)本模塊實(shí)現(xiàn)。要使用多個(gè)連接,首先要做的是創(chuàng)建這些連接。在這種情況下,連接命名成為必填項(xiàng)。

假設(shè)你有一個(gè)Album 實(shí)體存儲(chǔ)在他們自己的數(shù)據(jù)庫(kù)中。

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 {}

如果未為連接設(shè)置任何 name ,則該連接的名稱將設(shè)置為 default。請(qǐng)注意,不應(yīng)該有多個(gè)沒(méi)有名稱或同名的連接,否則它們會(huì)被覆蓋。

此時(shí),您的User 和 Album 實(shí)體中的每一個(gè)都已在各自的連接中注冊(cè)。通過(guò)此設(shè)置,您必須告訴 SequelizeModule.forFeature() 方法和 @InjectRepository() 裝飾器應(yīng)該使用哪種連接。如果不傳遞任何連接名稱,則使用 default 連接。

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

您也可以為給定的連接注入 Sequelize:

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

測(cè)試

在單元測(cè)試我們的應(yīng)用程序時(shí),我們通常希望避免任何數(shù)據(jù)庫(kù)連接,從而使我們的測(cè)試適合于獨(dú)立,并使它們的執(zhí)行過(guò)程盡可能快。但是我們的類可能依賴于從連接實(shí)例中提取的存儲(chǔ)庫(kù)。那是什么?解決方案是創(chuàng)建假模型。為了實(shí)現(xiàn)這一點(diǎn),我們?cè)O(shè)置了[自定義提供者]。事實(shí)上,每個(gè)注冊(cè)的模型都由 <ModelName>Model 令牌自動(dòng)表示,其中 ModelName 是模型類的名稱。

@nestjs/sequelize 包提供了基于給定模型返回準(zhǔn)備好 token 的 getModelToken() 函數(shù)。

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

現(xiàn)在, 將使用mockModel 作為 UsersModel。每當(dāng)任何提供程序使用 @InjectModel() 裝飾器請(qǐng)求 UserModel 時(shí), Nest 會(huì)使用注冊(cè)的 mockModel 對(duì)象。

異步配置

通常,您可能希望異步傳遞SequelizeModule選項(xiàng),而不是事先靜態(tài)傳遞它們。在這種情況下,使用 forRootAsync() 函數(shù),提供了幾種處理異步數(shù)據(jù)的方法。

第一種可能的方法是使用工廠函數(shù):

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

我們的工廠的行為與任何其他異步提供者一樣(例如,它可以是異步的,并且它能夠通過(guò)inject注入依賴)。

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

或者,您可以使用useClass語(yǔ)法。

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

上面的構(gòu)造將 SequelizeConfigService 在SequelizeModule內(nèi)部進(jìn)行實(shí)例化 ,并通過(guò)調(diào)用createSequelizeOptions()來(lái)創(chuàng)建一個(gè)選項(xiàng)對(duì)象。注意,這意味著 SequelizeConfigService 必須實(shí)現(xiàn) SequelizeOptionsFactory 的接口。如下所示:

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

為了防止在 SequelizeModule 中創(chuàng)建 SequelizeConfigService 并使用從不同模塊導(dǎo)入的提供程序,可以使用 useExisting 語(yǔ)法。

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

這個(gè)構(gòu)造與 useClass 的工作原理相同,但有一個(gè)關(guān)鍵的區(qū)別 — SequelizeModule 將查找導(dǎo)入的模塊來(lái)重用現(xiàn)有的 ConfigService,而不是實(shí)例化一個(gè)新的 ConfigService。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)