Nest không phụ thuộc vào một loại database cụ thể (database agnostic), cho phép bạn dễ dàng tích hợp với bất kỳ SQL hay NoSQL database nào. Bạn có nhiều lựa chọn tùy theo sở thích. Ở mức tổng quát nhất, kết nối Nest với database chỉ đơn giản là load driver Node.js phù hợp cho database đó, giống hệt như khi bạn làm với Express hay Fastify.
Bạn cũng có thể dùng trực tiếp bất kỳ library tích hợp database Node.js thông dụng nào hoặc ORM, chẳng hạn như MikroORM (xem MikroORM recipe), Sequelize (xem Sequelize integration), Knex.js (xem Knex.js tutorial), TypeORM, và Prisma (xem Prisma recipe), để làm việc ở một mức trừu tượng cao hơn.
Để tiện lợi, Nest cung cấp sẵn tích hợp chặt chẽ với TypeORM và Sequelize thông qua package @nestjs/typeorm và @nestjs/sequelize, sẽ được nói tới trong chương này, cùng với Mongoose qua @nestjs/mongoose, được nói tới ở chương này. Các tích hợp này cung cấp thêm những tính năng đặc thù của NestJS, như inject model/repository, khả năng test, và cấu hình bất đồng bộ để việc truy cập database bạn chọn trở nên dễ dàng hơn.
Ghi chú người dịch Nếu bạn từng dùng Sequelize/Knex thuần với Express hay database/sql với Go, bạn sẽ thấy các package @nestjs/* này thực chất chỉ là một lớp bọc (wrapper) mỏng: chúng biến các đối tượng connection/repository của thư viện gốc thành provider có thể inject qua DI, chứ không thay thế API của ORM gốc. Nói cách khác, bạn vẫn học và dùng TypeORM/Sequelize như bình thường, Nest chỉ lo phần “đưa nó vào IoC container” giúp bạn.
Tích hợp TypeORM (TypeORM Integration)
Để tích hợp với SQL và NoSQL database, Nest cung cấp package @nestjs/typeorm. TypeORM là Object Relational Mapper (ORM) trưởng thành nhất hiện có cho TypeScript. Vì được viết bằng TypeScript nên nó tích hợp rất tốt với framework Nest.
Để bắt đầu, trước tiên ta cài các dependency cần thiết. Trong chương này, ta sẽ minh họa bằng MySQL — một Relational DBMS phổ biến, nhưng TypeORM hỗ trợ nhiều relational database khác như PostgreSQL, Oracle, Microsoft SQL Server, SQLite, và cả NoSQL database như MongoDB. Quy trình trong chương này áp dụng như nhau cho mọi database mà TypeORM hỗ trợ. Bạn chỉ cần cài client API library tương ứng với database mình chọn.
$ npm install --save @nestjs/typeorm typeorm mysql2
Sau khi cài xong, ta có thể import TypeOrmModule vào root AppModule.
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 {}
Cảnh báo Không nên dùng synchronize: true trong môi trường production — nếu không bạn có thể mất dữ liệu production.
Phương thức forRoot() hỗ trợ toàn bộ các thuộc tính cấu hình được expose bởi constructor DataSource của package TypeORM. Ngoài ra, còn có một số thuộc tính cấu hình bổ sung được mô tả bên dưới.
retryAttempts |
Số lần thử kết nối lại database (mặc định: 10) |
retryDelay |
Thời gian chờ giữa các lần thử kết nối lại (ms) (mặc định: 3000) |
autoLoadEntities |
Nếu true, các entity sẽ được tự động load (mặc định: false) |
Gợi ý Tìm hiểu thêm về các data source option tại đây.
Sau khi hoàn tất, các đối tượng DataSource và EntityManager của TypeORM sẽ có thể inject được ở bất kỳ đâu trong toàn bộ project (không cần import thêm module nào), ví dụ:
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;
}
}
Repository pattern
TypeORM hỗ trợ repository design pattern, nên mỗi entity sẽ có repository riêng của nó. Các repository này có thể lấy được từ database data source.
Để tiếp tục ví dụ, ta cần ít nhất một entity. Hãy định nghĩa entity User.
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
Gợi ý Tìm hiểu thêm về entity trong tài liệu TypeORM.
File entity User nằm trong thư mục users. Thư mục này chứa toàn bộ các file liên quan tới UsersModule. Bạn có thể tự quyết định lưu file model ở đâu, tuy nhiên chúng tôi khuyến nghị nên tạo chúng gần với domain tương ứng, trong thư mục module liên quan.
Để bắt đầu dùng entity User, ta cần cho TypeORM biết về nó bằng cách thêm vào mảng entities trong option của phương thức forRoot() (trừ khi bạn dùng static glob path):
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity.js';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
Tiếp theo, hãy xem UsersModule:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service.js';
import { UsersController } from './users.controller.js';
import { User } from './user.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Module này dùng phương thức forFeature() để định nghĩa những repository nào được đăng ký trong scope hiện tại. Với đó, ta có thể inject UsersRepository vào UsersService bằng decorator @InjectRepository():
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity.js';
@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.js';
@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);
}
}
Lưu ý Đừng quên import UsersModule vào root AppModule.
Nếu muốn dùng repository ở ngoài module đã import TypeOrmModule.forFeature, bạn cần re-export các provider mà nó sinh ra.
Bạn có thể làm điều này bằng cách export cả module, như sau:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule]
})
export class UsersModule {}
Giờ nếu ta import UsersModule vào UserHttpModule, ta có thể dùng @InjectRepository(User) trong các provider của module sau.
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module.js';
import { UsersService } from './users.service.js';
import { UsersController } from './users.controller.js';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Relations (quan hệ)
Relations là các liên kết được thiết lập giữa hai hay nhiều bảng. Relations dựa trên các trường chung của mỗi bảng, thường liên quan tới primary key và foreign key.
Có ba loại relation:
One-to-one |
Mỗi dòng trong bảng chính chỉ có duy nhất một dòng liên kết ở bảng phụ. Dùng decorator @OneToOne() để định nghĩa loại relation này. |
One-to-many / Many-to-one |
Mỗi dòng trong bảng chính có một hoặc nhiều dòng liên quan ở bảng phụ. Dùng decorator @OneToMany() và @ManyToOne() để định nghĩa loại relation này. |
Many-to-many |
Mỗi dòng trong bảng chính có nhiều dòng liên quan ở bảng phụ, và mỗi bản ghi trong bảng phụ cũng có nhiều dòng liên quan ở bảng chính. Dùng decorator @ManyToMany() để định nghĩa loại relation này. |
Để định nghĩa relation trong entity, dùng decorator tương ứng. Ví dụ, để định nghĩa rằng mỗi User có thể có nhiều photo, dùng decorator @OneToMany().
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity.js';
@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[];
}
Gợi ý Để tìm hiểu thêm về relation trong TypeORM, xem tài liệu TypeORM.
Auto-load entities (tự động load entity)
Việc thêm entity thủ công vào mảng entities trong data source options có thể khá tốn công. Ngoài ra, việc tham chiếu entity từ root module còn phá vỡ ranh giới domain của ứng dụng và làm lộ chi tiết triển khai (implementation detail) sang các phần khác của ứng dụng. Để giải quyết vấn đề này, có một giải pháp thay thế. Để tự động load entity, đặt thuộc tính autoLoadEntities trong object cấu hình (truyền vào phương thức forRoot()) thành true, như dưới đây:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
Khi bật option này, mọi entity được đăng ký qua phương thức forFeature() sẽ tự động được thêm vào mảng entities của object cấu hình.
Cảnh báo Lưu ý rằng những entity không được đăng ký qua phương thức forFeature(), mà chỉ được tham chiếu từ entity khác (thông qua relationship), sẽ không được đưa vào bởi cơ chế autoLoadEntities.
Tách riêng định nghĩa entity (Separating entity definition)
Bạn có thể định nghĩa entity và các cột của nó ngay trong model, bằng decorator. Nhưng một số người thích định nghĩa entity và các cột trong các file riêng biệt, dùng “entity schemas”.
import { EntitySchema } from 'typeorm';
import { User } from './user.entity.js';
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', // tên của PhotoSchema
},
},
});
error Cảnh báo Nếu bạn cung cấp option target, giá trị của option name phải trùng với tên của class được nhắm tới (target class).
Nếu bạn không cung cấp target, bạn có thể dùng bất kỳ tên nào.
Nest cho phép bạn dùng instance EntitySchema ở bất cứ chỗ nào cần một Entity, ví dụ:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema.js';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';
@Module({
imports: [TypeOrmModule.forFeature([UserSchema])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
TypeORM Transactions
Một database transaction đại diện cho một đơn vị công việc (unit of work) được thực hiện trong một hệ quản trị database đối với database đó, và được xử lý theo cách nhất quán, đáng tin cậy, độc lập với các transaction khác. Transaction thường đại diện cho bất kỳ thay đổi nào trong database (tìm hiểu thêm).
Có nhiều chiến lược khác nhau để xử lý TypeORM transaction. Chúng tôi khuyến nghị dùng class QueryRunner vì nó cho phép kiểm soát toàn bộ transaction.
Trước tiên, ta cần inject object DataSource vào một class theo cách thông thường:
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
}
Gợi ý Class DataSource được import từ package typeorm.
Giờ, ta có thể dùng object này để tạo một 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) {
// vì có lỗi nên rollback lại các thay đổi đã thực hiện
await queryRunner.rollbackTransaction();
} finally {
// bạn cần release queryRunner đã được khởi tạo thủ công
await queryRunner.release();
}
}
Gợi ý Lưu ý rằng dataSource chỉ được dùng để tạo QueryRunner. Tuy nhiên, để test class này bạn sẽ cần mock toàn bộ object DataSource (vốn expose khá nhiều phương thức). Do đó, chúng tôi khuyến nghị dùng một helper factory class (ví dụ: QueryRunnerFactory) và định nghĩa một interface với tập hợp phương thức giới hạn, chỉ đủ để duy trì transaction. Kỹ thuật này giúp việc mock các phương thức này trở nên khá đơn giản.
Ghi chú người dịch Cách dùng QueryRunner ở đây tương đương với việc bạn tự BEGIN/COMMIT/ROLLBACK transaction thủ công bằng driver SQL thuần (hoặc db.Begin() trong Go/sql.DB), chỉ khác là TypeORM đóng gói nó thành object. Cạm bẫy thực tế hay gặp: quên gọi queryRunner.release() trong khối finally sẽ làm rò rỉ connection trong connection pool, tích lũy dần sẽ khiến ứng dụng hết connection khả dụng dưới tải cao — lỗi này rất khó phát hiện khi test local vì pool nhỏ ít khi cạn.
Ngoài ra, bạn cũng có thể dùng cách tiếp cận kiểu callback với phương thức transaction của object DataSource (đọc thêm).
async createMany(users: User[]) {
await this.dataSource.transaction(async manager => {
await manager.save(users[0]);
await manager.save(users[1]);
});
}
Subscribers
Với TypeORM subscribers, bạn có thể lắng nghe các sự kiện cụ thể của entity.
import {
DataSource,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity.js';
@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);
}
}
Cảnh báo Event subscriber không thể là request-scoped.
Giờ, thêm class UserSubscriber vào mảng providers:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity.js';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';
import { UserSubscriber } from './user.subscriber.js';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, UserSubscriber],
controllers: [UsersController],
})
export class UsersModule {}
Migrations
Migrations cung cấp cách để cập nhật dần schema database, giữ nó đồng bộ với data model của ứng dụng trong khi vẫn giữ nguyên dữ liệu hiện có trong database. Để generate, chạy, và revert migration, TypeORM cung cấp một CLI riêng.
Các class migration tách biệt hoàn toàn với source code của ứng dụng Nest. Vòng đời của chúng do TypeORM CLI quản lý. Vì vậy, bạn không thể tận dụng dependency injection và các tính năng đặc thù khác của Nest trong migration. Để tìm hiểu thêm về migration, theo dõi hướng dẫn trong tài liệu TypeORM.
Ghi chú người dịch Đây là điểm khác biệt quan trọng so với Laravel — ở Laravel, migration vẫn chạy trong “vũ trụ” của framework (có thể dùng model, helper…). Còn migration của TypeORM chạy độc lập hoàn toàn ngoài Nest IoC container, nên đừng cố @InjectRepository hay inject ConfigService vào file migration — nó sẽ không hoạt động vì TypeORM CLI khởi tạo migration bằng cơ chế riêng của nó, không qua Nest.
Multiple databases (nhiều database)
Một số project cần nhiều kết nối database. Module này cũng hỗ trợ điều đó. Để làm việc với nhiều kết nối, trước tiên hãy tạo các kết nối. Trong trường hợp này, việc đặt tên cho data source trở thành bắt buộc.
Giả sử bạn có entity Album được lưu trong database riêng của nó.
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 {}
Lưu ý Nếu bạn không đặt name cho một data source, tên của nó sẽ mặc định là default. Lưu ý rằng bạn không nên có nhiều kết nối không tên, hoặc trùng tên, nếu không chúng sẽ bị ghi đè lên nhau.
Lưu ý Nếu bạn dùng TypeOrmModule.forRootAsync, bạn phải đồng thời đặt tên data source ở ngoài useFactory. Ví dụ:
TypeOrmModule.forRootAsync({
name: 'albumsConnection',
useFactory: ...,
inject: ...,
}),
Xem issue này để biết thêm chi tiết.
Tới đây, bạn đã có entity User và Album được đăng ký với data source riêng của chúng. Với thiết lập này, bạn cần cho phương thức TypeOrmModule.forFeature() và decorator @InjectRepository() biết nên dùng data source nào. Nếu bạn không truyền tên data source, data source default sẽ được dùng.
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
Bạn cũng có thể inject DataSource hoặc EntityManager cho một data source cụ thể:
@Injectable()
export class AlbumsService {
constructor(
@InjectDataSource('albumsConnection')
private dataSource: DataSource,
@InjectEntityManager('albumsConnection')
private entityManager: EntityManager,
) {}
}
Cũng có thể inject bất kỳ DataSource nào vào provider:
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsConnection: DataSource) => {
return new AlbumsService(albumsConnection);
},
inject: [getDataSourceToken('albumsConnection')],
},
],
})
export class AlbumsModule {}
Testing
Khi test unit cho một ứng dụng, ta thường muốn tránh việc tạo kết nối database thật, để giữ cho test suite độc lập và quá trình chạy test nhanh nhất có thể. Nhưng các class của ta có thể phụ thuộc vào repository được lấy ra từ instance data source (connection). Vậy ta xử lý thế nào? Giải pháp là tạo mock repository. Để làm được điều đó, ta thiết lập custom provider. Mỗi repository đã đăng ký được tự động đại diện bởi một token <EntityName>Repository, trong đó EntityName là tên class entity của bạn.
Package @nestjs/typeorm expose hàm getRepositoryToken(), trả về một token đã chuẩn bị sẵn dựa trên entity được truyền vào.
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
Giờ một mockRepository thay thế sẽ được dùng làm UsersRepository. Bất cứ khi nào một class yêu cầu UsersRepository thông qua decorator @InjectRepository(), Nest sẽ dùng object mockRepository đã đăng ký.
Async configuration (cấu hình bất đồng bộ)
Bạn có thể muốn truyền option của repository module một cách bất đồng bộ thay vì tĩnh (static). Trong trường hợp này, dùng phương thức forRootAsync(), cung cấp nhiều cách để xử lý cấu hình bất đồng bộ.
Một cách là dùng factory function:
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
});
Factory của ta hoạt động giống bất kỳ asynchronous provider nào khác (ví dụ: có thể là async, và có thể inject dependency qua 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],
});
Ngoài ra, bạn có thể dùng cú pháp useClass:
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
});
Cấu trúc trên sẽ khởi tạo TypeOrmConfigService bên trong TypeOrmModule và dùng nó để cung cấp object option bằng cách gọi createTypeOrmOptions(). Lưu ý rằng điều này có nghĩa TypeOrmConfigService phải implement interface TypeOrmOptionsFactory, như dưới đây:
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
};
}
}
Để tránh việc tạo TypeOrmConfigService bên trong TypeOrmModule và dùng một provider được import từ module khác, bạn có thể dùng cú pháp useExisting.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Cấu trúc này hoạt động giống useClass, chỉ khác một điểm quan trọng — TypeOrmModule sẽ tìm trong các module đã import để tái sử dụng ConfigService đã tồn tại thay vì khởi tạo một instance mới.
Gợi ý Đảm bảo rằng thuộc tính name được định nghĩa cùng cấp với thuộc tính useFactory, useClass, hoặc useValue. Điều này giúp Nest đăng ký data source đúng dưới injection token phù hợp.
Custom DataSource Factory
Kết hợp với cấu hình bất đồng bộ dùng useFactory, useClass, hoặc useExisting, bạn có thể chỉ định (không bắt buộc) một hàm dataSourceFactory, cho phép bạn cung cấp TypeORM data source của riêng mình thay vì để TypeOrmModule tự tạo data source.
dataSourceFactory nhận DataSourceOptions của TypeORM đã được cấu hình bằng useFactory, useClass, hoặc useExisting trong quá trình cấu hình bất đồng bộ, và trả về một Promise resolve ra một TypeORM DataSource.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
// Dùng useFactory, useClass, hoặc useExisting
// để cấu hình 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 nhận DataSourceOptions đã được cấu hình
// và trả về Promise<DataSource>.
dataSourceFactory: async (options) => {
const dataSource = await new DataSource(options).initialize();
return dataSource;
},
});
Gợi ý Class DataSource được import từ package typeorm.
Example (ví dụ)
Một ví dụ hoạt động đầy đủ có sẵn tại đây.
Tích hợp Sequelize (Sequelize Integration)
Một lựa chọn thay thế cho TypeORM là dùng ORM Sequelize với package @nestjs/sequelize. Ngoài ra, ta còn tận dụng package sequelize-typescript, cung cấp một tập hợp decorator bổ sung để định nghĩa entity theo kiểu khai báo (declarative).
Để bắt đầu, trước tiên ta cài các dependency cần thiết. Trong chương này, ta sẽ minh họa bằng MySQL — một Relational DBMS phổ biến, nhưng Sequelize hỗ trợ nhiều relational database như PostgreSQL, MySQL, Microsoft SQL Server, SQLite, và MariaDB. Quy trình trong chương này áp dụng như nhau cho mọi database mà Sequelize hỗ trợ. Bạn chỉ cần cài client API library tương ứng với database mình chọn.
$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize
Sau khi cài xong, ta có thể import SequelizeModule vào root AppModule.
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 {}
Phương thức forRoot() hỗ trợ toàn bộ các thuộc tính cấu hình được expose bởi constructor của Sequelize (đọc thêm). Ngoài ra, còn có một số thuộc tính cấu hình bổ sung được mô tả bên dưới.
retryAttempts |
Số lần thử kết nối lại database (mặc định: 10) |
retryDelay |
Thời gian chờ giữa các lần thử kết nối lại (ms) (mặc định: 3000) |
autoLoadModels |
Nếu true, các model sẽ được tự động load (mặc định: false) |
keepConnectionAlive |
Nếu true, kết nối sẽ không bị đóng khi ứng dụng shutdown (mặc định: false) |
synchronize |
Nếu true, các model được tự động load sẽ được đồng bộ (mặc định: true) |
Sau khi hoàn tất, object Sequelize sẽ có thể inject được ở bất kỳ đâu trong toàn bộ project (không cần import thêm module nào), ví dụ:
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;
}
}
Models
Sequelize implement Active Record pattern. Với pattern này, bạn dùng trực tiếp model class để tương tác với database. Để tiếp tục ví dụ, ta cần ít nhất một model. Hãy định nghĩa model User.
import { Column, Model, Table } from 'sequelize-typescript';
@Table
export class User extends Model {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
}
Gợi ý Tìm hiểu thêm về các decorator có sẵn tại đây.
Ghi chú người dịch Active Record pattern (mỗi instance model tự biết cách lưu/xóa chính nó, kiểu user.save(), user.destroy()) khác về bản chất với Data Mapper pattern mà TypeORM’s Repository dùng ở trên (repository tách biệt, usersRepository.save(user)). Nếu bạn từng dùng Eloquent của Laravel, Active Record của Sequelize sẽ rất quen thuộc; còn nếu quen Repository pattern kiểu Go (UserRepository.Save(ctx, user)), cách tiếp cận của TypeORM sẽ gần với tư duy của bạn hơn. Đây không chỉ là khác biệt cú pháp — nó ảnh hưởng tới việc test: Active Record khó mock hơn vì logic lưu trữ nằm ngay trong entity.
File model User nằm trong thư mục users. Thư mục này chứa toàn bộ các file liên quan tới UsersModule. Bạn có thể tự quyết định lưu file model ở đâu, tuy nhiên chúng tôi khuyến nghị nên tạo chúng gần với domain tương ứng, trong thư mục module liên quan.
Để bắt đầu dùng model User, ta cần cho Sequelize biết về nó bằng cách thêm vào mảng models trong option của phương thức forRoot():
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model.js';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [User],
}),
],
})
export class AppModule {}
Tiếp theo, hãy xem UsersModule:
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model.js';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';
@Module({
imports: [SequelizeModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Module này dùng phương thức forFeature() để định nghĩa những model nào được đăng ký trong scope hiện tại. Với đó, ta có thể inject UserModel vào UsersService bằng decorator @InjectModel():
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model.js';
@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.js';
@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();
}
}
Lưu ý Đừng quên import UsersModule vào root AppModule.
Nếu muốn dùng model ở ngoài module đã import SequelizeModule.forFeature, bạn cần re-export các provider mà nó sinh ra.
Bạn có thể làm điều này bằng cách export cả module, như sau:
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity.js';
@Module({
imports: [SequelizeModule.forFeature([User])],
exports: [SequelizeModule]
})
export class UsersModule {}
Giờ nếu ta import UsersModule vào UserHttpModule, ta có thể dùng @InjectModel(User) trong các provider của module sau.
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module.js';
import { UsersService } from './users.service.js';
import { UsersController } from './users.controller.js';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Relations (quan hệ)
Relations là các liên kết được thiết lập giữa hai hay nhiều bảng. Relations dựa trên các trường chung của mỗi bảng, thường liên quan tới primary key và foreign key.
Có ba loại relation:
One-to-one |
Mỗi dòng trong bảng chính chỉ có duy nhất một dòng liên kết ở bảng phụ |
One-to-many / Many-to-one |
Mỗi dòng trong bảng chính có một hoặc nhiều dòng liên quan ở bảng phụ |
Many-to-many |
Mỗi dòng trong bảng chính có nhiều dòng liên quan ở bảng phụ, và mỗi bản ghi trong bảng phụ cũng có nhiều dòng liên quan ở bảng chính |
Để định nghĩa relation trong model, dùng decorator tương ứng. Ví dụ, để định nghĩa rằng mỗi User có thể có nhiều photo, dùng decorator @HasMany().
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model.js';
@Table
export class User extends Model {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
@HasMany(() => Photo)
photos: Photo[];
}
Gợi ý Để tìm hiểu thêm về association trong Sequelize, đọc chương này.
Auto-load models (tự động load model)
Việc thêm model thủ công vào mảng models trong connection options có thể khá tốn công. Ngoài ra, việc tham chiếu model từ root module còn phá vỡ ranh giới domain của ứng dụng và làm lộ chi tiết triển khai sang các phần khác của ứng dụng. Để giải quyết vấn đề này, hãy tự động load model bằng cách đặt cả hai thuộc tính autoLoadModels và synchronize trong object cấu hình (truyền vào phương thức forRoot()) thành true, như dưới đây:
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
...
autoLoadModels: true,
synchronize: true,
}),
],
})
export class AppModule {}
Khi bật option này, mọi model được đăng ký qua phương thức forFeature() sẽ tự động được thêm vào mảng models của object cấu hình.
Cảnh báo Lưu ý rằng những model không được đăng ký qua phương thức forFeature(), mà chỉ được tham chiếu từ model khác (thông qua association), sẽ không được đưa vào.
Sequelize Transactions
Một database transaction đại diện cho một đơn vị công việc được thực hiện trong một hệ quản trị database đối với database đó, và được xử lý theo cách nhất quán, đáng tin cậy, độc lập với các transaction khác. Transaction thường đại diện cho bất kỳ thay đổi nào trong database (tìm hiểu thêm).
Có nhiều chiến lược khác nhau để xử lý Sequelize transaction. Dưới đây là một ví dụ triển khai managed transaction (auto-callback).
Trước tiên, ta cần inject object Sequelize vào một class theo cách thông thường:
@Injectable()
export class UsersService {
constructor(private sequelize: Sequelize) {}
}
Gợi ý Class Sequelize được import từ package sequelize-typescript.
Giờ, ta có thể dùng object này để tạo một 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) {
// Transaction đã bị rollback
// err là bất cứ thứ gì làm reject promise chain trả về cho transaction callback
}
}
Gợi ý Lưu ý rằng instance Sequelize chỉ được dùng để bắt đầu transaction. Tuy nhiên, để test class này bạn sẽ cần mock toàn bộ object Sequelize (vốn expose khá nhiều phương thức). Do đó, chúng tôi khuyến nghị dùng một helper factory class (ví dụ: TransactionRunner) và định nghĩa một interface với tập hợp phương thức giới hạn, chỉ đủ để duy trì transaction. Kỹ thuật này giúp việc mock các phương thức này trở nên khá đơn giản.
Migrations
Migrations cung cấp cách để cập nhật dần schema database, giữ nó đồng bộ với data model của ứng dụng trong khi vẫn giữ nguyên dữ liệu hiện có trong database. Để generate, chạy, và revert migration, Sequelize cung cấp một CLI riêng.
Các class migration tách biệt hoàn toàn với source code của ứng dụng Nest. Vòng đời của chúng do Sequelize CLI quản lý. Vì vậy, bạn không thể tận dụng dependency injection và các tính năng đặc thù khác của Nest trong migration. Để tìm hiểu thêm về migration, theo dõi hướng dẫn trong tài liệu Sequelize.
Multiple databases (nhiều database)
Một số project cần nhiều kết nối database. Module này cũng hỗ trợ điều đó. Để làm việc với nhiều kết nối, trước tiên hãy tạo các kết nối. Trong trường hợp này, việc đặt tên cho connection trở thành bắt buộc.
Giả sử bạn có entity Album được lưu trong database riêng của nó.
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 {}
Lưu ý Nếu bạn không đặt name cho một connection, tên của nó sẽ mặc định là default. Lưu ý rằng bạn không nên có nhiều kết nối không tên, hoặc trùng tên, nếu không chúng sẽ bị ghi đè lên nhau.
Tới đây, bạn đã có model User và Album được đăng ký với connection riêng của chúng. Với thiết lập này, bạn cần cho phương thức SequelizeModule.forFeature() và decorator @InjectModel() biết nên dùng connection nào. Nếu bạn không truyền tên connection, connection default sẽ được dùng.
@Module({
imports: [
SequelizeModule.forFeature([User]),
SequelizeModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
Bạn cũng có thể inject instance Sequelize cho một connection cụ thể:
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private sequelize: Sequelize,
) {}
}
Cũng có thể inject bất kỳ instance Sequelize nào vào provider:
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsSequelize: Sequelize) => {
return new AlbumsService(albumsSequelize);
},
inject: [getDataSourceToken('albumsConnection')],
},
],
})
export class AlbumsModule {}
Testing
Khi test unit cho một ứng dụng, ta thường muốn tránh việc tạo kết nối database thật, để giữ cho test suite độc lập và quá trình chạy test nhanh nhất có thể. Nhưng các class của ta có thể phụ thuộc vào model được lấy ra từ instance connection. Vậy ta xử lý thế nào? Giải pháp là tạo mock model. Để làm được điều đó, ta thiết lập custom provider. Mỗi model đã đăng ký được tự động đại diện bởi một token <ModelName>Model, trong đó ModelName là tên class model của bạn.
Package @nestjs/sequelize expose hàm getModelToken(), trả về một token đã chuẩn bị sẵn dựa trên model được truyền vào.
@Module({
providers: [
UsersService,
{
provide: getModelToken(User),
useValue: mockModel,
},
],
})
export class UsersModule {}
Giờ một mockModel thay thế sẽ được dùng làm UserModel. Bất cứ khi nào một class yêu cầu UserModel thông qua decorator @InjectModel(), Nest sẽ dùng object mockModel đã đăng ký.
Async configuration (cấu hình bất đồng bộ)
Bạn có thể muốn truyền option của SequelizeModule một cách bất đồng bộ thay vì tĩnh (static). Trong trường hợp này, dùng phương thức forRootAsync(), cung cấp nhiều cách để xử lý cấu hình bất đồng bộ.
Một cách là dùng factory function:
SequelizeModule.forRootAsync({
useFactory: () => ({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
}),
});
Factory của ta hoạt động giống bất kỳ asynchronous provider nào khác (ví dụ: có thể là async, và có thể inject dependency qua 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],
});
Ngoài ra, bạn có thể dùng cú pháp useClass:
SequelizeModule.forRootAsync({
useClass: SequelizeConfigService,
});
Cấu trúc trên sẽ khởi tạo SequelizeConfigService bên trong SequelizeModule và dùng nó để cung cấp object option bằng cách gọi createSequelizeOptions(). Lưu ý rằng điều này có nghĩa SequelizeConfigService phải implement interface SequelizeOptionsFactory, như dưới đây:
@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
createSequelizeOptions(): SequelizeModuleOptions {
return {
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
};
}
}
Để tránh việc tạo SequelizeConfigService bên trong SequelizeModule và dùng một provider được import từ module khác, bạn có thể dùng cú pháp useExisting.
SequelizeModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Cấu trúc này hoạt động giống useClass, chỉ khác một điểm quan trọng — SequelizeModule sẽ tìm trong các module đã import để tái sử dụng ConfigService đã tồn tại thay vì khởi tạo một instance mới.
Example (ví dụ)
Một ví dụ hoạt động đầy đủ có sẵn tại đây.