☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Kỹ thuật (Techniques)

Mongo

Bản dịch từ docs.nestjs.com/techniques/mongodb

Nest hỗ trợ hai cách để tích hợp với database MongoDB. Bạn có thể dùng module TypeORM có sẵn được mô tả ở đây, vốn có connector cho MongoDB, hoặc dùng Mongoose, công cụ mô hình hóa object MongoDB phổ biến nhất. Trong chương này chúng ta sẽ nói về cách thứ hai, sử dụng package chuyên dụng @nestjs/mongoose.

Bắt đầu bằng cách cài các dependency cần thiết:

$ npm i @nestjs/mongoose mongoose

Sau khi cài xong, ta có thể import MongooseModule vào AppModule gốc.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}

Phương thức forRoot() nhận cùng một object cấu hình như mongoose.connect() từ package Mongoose, được mô tả ở đây.

Inject model (Model injection)

Với Mongoose, mọi thứ đều bắt nguồn từ Schema. Mỗi schema ánh xạ tới một collection MongoDB và định nghĩa cấu trúc của các document trong collection đó. Schema được dùng để định nghĩa Model. Model chịu trách nhiệm tạo và đọc document từ database MongoDB bên dưới.

Schema có thể được tạo bằng decorator của NestJS, hoặc tạo thủ công bằng chính Mongoose. Dùng decorator để tạo schema giúp giảm đáng kể code khung (boilerplate) và cải thiện khả năng đọc code.

Hãy định nghĩa CatSchema:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type CatDocument = HydratedDocument<Cat>;

@Schema()
export class Cat {
  @Prop()
  name: string;

  @Prop()
  age: number;

  @Prop()
  breed: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);

Gợi ý Lưu ý bạn cũng có thể tạo ra một schema definition thô bằng class DefinitionsFactory (từ nestjs/mongoose). Điều này cho phép bạn chỉnh sửa thủ công schema definition được sinh ra dựa trên metadata bạn cung cấp. Hữu ích cho một số trường hợp đặc biệt mà việc biểu diễn mọi thứ bằng decorator gặp khó khăn.

Decorator @Schema() đánh dấu một class là một schema definition. Nó ánh xạ class Cat của chúng ta tới một collection MongoDB cùng tên, nhưng có thêm chữ “s” ở cuối - vậy nên tên collection mongo cuối cùng sẽ là cats. Decorator này nhận một tham số tùy chọn duy nhất là một object tùy chọn schema. Hãy coi nó như object mà bạn thường truyền làm tham số thứ hai của constructor class mongoose.Schema (ví dụ: new mongoose.Schema(_, options)). Để tìm hiểu thêm về các tùy chọn schema, xem chương này.

Ghi chú người dịch Nếu bạn từng dùng Mongoose thuần (không qua Nest), đây chính là new mongoose.Schema({...}) mà bạn hay viết tay - Nest chỉ “dịch” class + decorator sang cùng một object đó lúc runtime nhờ reflection metadata của TypeScript. Điều này tương tự cách một ORM như TypeORM hay Prisma sinh schema từ class, nhưng bản chất Mongoose vẫn document-based (schema-less ở tầng MongoDB), khác hẳn với schema cứng của SQL.

Decorator @Prop() định nghĩa một property trong document. Ví dụ, trong schema definition ở trên, chúng ta đã định nghĩa ba property: name, age, và breed. Các schema type cho các property này được tự động suy ra nhờ khả năng TypeScript metadata (và reflection). Tuy nhiên, trong các trường hợp phức tạp hơn mà kiểu dữ liệu không thể được suy luận ngầm (ví dụ, mảng hoặc cấu trúc object lồng nhau), kiểu dữ liệu phải được chỉ định tường minh, như sau:

@Prop([String])
tags: string[];

Ngoài ra, decorator @Prop() cũng nhận một tham số là object tùy chọn (đọc thêm về các tùy chọn có sẵn). Với nó, bạn có thể chỉ định một property là bắt buộc hay không, xác định giá trị mặc định, hoặc đánh dấu nó là immutable. Ví dụ:

@Prop({ required: true })
name: string;

Trong trường hợp bạn muốn chỉ định quan hệ tới một model khác, để sau này populate, bạn cũng có thể dùng decorator @Prop(). Ví dụ, nếu Cat có một Owner được lưu trong một collection khác tên là owners, property đó nên có type và ref. Ví dụ:

import mongoose from 'mongoose';
import { Owner } from '../owners/schemas/owner.schema.js';

// bên trong định nghĩa class
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
owner: Owner;

Trong trường hợp có nhiều owner, cấu hình property của bạn nên như sau:

@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' }] })
owners: Owner[];

Nếu bạn không có ý định luôn luôn populate một reference tới collection khác, hãy cân nhắc dùng mongoose.Types.ObjectId làm type thay thế:

@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Owner' })
// Điều này đảm bảo field không bị nhầm lẫn với một reference đã được populate
owner: mongoose.Types.ObjectId;

Sau đó, khi bạn cần populate nó một cách có chọn lọc, bạn có thể dùng một hàm repository chỉ định đúng type:

import { Owner } from './schemas/owner.schema.js';

// ví dụ: bên trong một service hoặc repository
async findAllPopulated() {
  return this.catModel.find().populate<{ owner: Owner }>("owner");
}

Gợi ý Nếu không có document ngoại (foreign document) nào để populate, type có thể là Owner | null, tùy vào cấu hình Mongoose của bạn. Hoặc, nó có thể ném ra lỗi, trong trường hợp đó type sẽ là Owner.

Cuối cùng, schema definition thô cũng có thể được truyền vào decorator. Điều này hữu ích khi, ví dụ, một property đại diện cho một object lồng nhau không được định nghĩa như một class. Để làm điều này, dùng hàm raw() từ package @nestjs/mongoose, như sau:

@Prop(raw({
  firstName: { type: String },
  lastName: { type: String }
}))
details: Record<string, any>;

Ngoài ra, nếu bạn không thích dùng decorator, bạn có thể định nghĩa schema thủ công. Ví dụ:

export const CatSchema = new mongoose.Schema({
  name: String,
  age: Number,
  breed: String,
});

File cat.schema nằm trong một thư mục con của thư mục cats, nơi chúng ta cũng định nghĩa CatsModule. Dù bạn có thể lưu file schema ở bất cứ đâu bạn muốn, chúng tôi khuyến nghị lưu chúng gần các đối tượng domain liên quan, trong thư mục module tương ứng.

Hãy xem CatsModule:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
import { Cat, CatSchema } from './schemas/cat.schema.js';

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

MongooseModule cung cấp phương thức forFeature() để cấu hình module, bao gồm việc định nghĩa những model nào sẽ được đăng ký trong scope hiện tại. Nếu bạn cũng muốn dùng các model đó ở một module khác, hãy thêm MongooseModule vào phần exports của CatsModule và import CatsModule vào module kia.

Sau khi đăng ký schema, bạn có thể inject model Cat vào CatsService bằng decorator @InjectModel():

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat } from './schemas/cat.schema.js';
import { CreateCatDto } from './dto/create-cat.dto.js';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<Cat>) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }
}
import { Model } from 'mongoose';
import { Injectable, Dependencies } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Cat } from './schemas/cat.schema.js';

@Injectable()
@Dependencies(getModelToken(Cat.name))
export class CatsService {
  constructor(catModel) {
    this.catModel = catModel;
  }

  async create(createCatDto) {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll() {
    return this.catModel.find().exec();
  }
}

Ghi chú người dịch So với Go, đây gần giống việc bạn định nghĩa một struct rồi truyền nó cho một hàm khởi tạo repository (ví dụ NewCatRepository(db *mongo.Database)), chỉ khác là Nest tự động “tiêm” instance Model<Cat> vào constructor thay vì bạn phải tự khởi tạo và truyền tay. @InjectModel(Cat.name) chính là injection token - Nest tra trong IoC container xem ai đã đăng ký provider ứng với token đó (được đăng ký ở bước forFeature() phía trên) rồi đưa vào.

Connection

Đôi khi bạn cần truy cập trực tiếp object Mongoose Connection gốc. Ví dụ, bạn có thể muốn gọi các API gốc trên connection object. Bạn có thể inject Mongoose Connection bằng decorator @InjectConnection() như sau:

import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

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

Session

Để bắt đầu một session với Mongoose, nên inject database connection bằng @InjectConnection thay vì gọi trực tiếp mongoose.startSession(). Cách này giúp tích hợp tốt hơn với hệ thống dependency injection của NestJS, đảm bảo việc quản lý connection đúng đắn.

Đây là ví dụ về cách bắt đầu một session:

import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

@Injectable()
export class CatsService {
  constructor(@InjectConnection() private readonly connection: Connection) {}

  async startTransaction() {
    const session = await this.connection.startSession();
    session.startTransaction();
    // Logic transaction của bạn ở đây
  }
}

Trong ví dụ này, @InjectConnection() được dùng để inject Mongoose connection vào service. Sau khi connection được inject, bạn có thể dùng connection.startSession() để bắt đầu một session mới. Session này có thể được dùng để quản lý các database transaction, đảm bảo các thao tác atomic trên nhiều query. Sau khi bắt đầu session, nhớ commit hoặc abort transaction tùy theo logic của bạn.

Ghi chú người dịch Nếu bạn từng dùng transaction thủ công trong PHP/Laravel (DB::beginTransaction() / commit() / rollBack()) hay Go (db.BeginTx), cơ chế ở đây tương tự: startSession() + startTransaction() mở một giao dịch, và bạn phải tự gọi commitTransaction()/abortTransaction() - Nest không tự động commit giúp bạn. Một cạm bẫy hay gặp là quên gọi session.endSession() sau khi xong, dẫn tới rò rỉ session/connection pool khi chạy nhiều request đồng thời.

Nhiều database (Multiple databases)

Một số dự án cần nhiều kết nối database. Điều này cũng có thể thực hiện được với module này. Để làm việc với nhiều connection, trước tiên hãy tạo các connection. Trong trường hợp này, việc đặt tên connection trở thành bắt buộc.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/test', {
      connectionName: 'cats',
    }),
    MongooseModule.forRoot('mongodb://localhost/users', {
      connectionName: 'users',
    }),
  ],
})
export class AppModule {}

Cảnh báo Lưu ý bạn không nên có nhiều connection không tên, hoặc cùng tên, nếu không chúng sẽ bị ghi đè lên nhau.

Với cấu hình này, bạn phải cho hàm MongooseModule.forFeature() biết connection nào cần được sử dụng.

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }], 'cats'),
  ],
})
export class CatsModule {}

Bạn cũng có thể inject Connection cho một connection cụ thể:

import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

@Injectable()
export class CatsService {
  constructor(@InjectConnection('cats') private connection: Connection) {}
}

Để inject một Connection cho trước vào một custom provider (ví dụ, factory provider), dùng hàm getConnectionToken() truyền vào tên của connection làm tham số.

{
  provide: CatsService,
  useFactory: (catsConnection: Connection) => {
    return new CatsService(catsConnection);
  },
  inject: [getConnectionToken('cats')],
}

Nếu bạn chỉ muốn inject model từ một database có tên, bạn có thể dùng tên connection làm tham số thứ hai cho decorator @InjectModel().

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}
@Injectable()
@Dependencies(getModelToken(Cat.name, 'cats'))
export class CatsService {
  constructor(catModel) {
    this.catModel = catModel;
  }
}

Hook (middleware)

Middleware (còn gọi là pre và post hook) là các hàm được truyền quyền điều khiển trong quá trình thực thi các hàm bất đồng bộ. Middleware được chỉ định ở cấp độ schema và hữu ích khi viết plugin (nguồn). Gọi pre() hoặc post() sau khi đã compile một model sẽ không có tác dụng trong Mongoose. Để đăng ký một hook trước khi model được đăng ký, dùng phương thức forFeatureAsync() của MongooseModule cùng với một factory provider (tức useFactory). Với kỹ thuật này, bạn có thể truy cập một schema object, rồi dùng phương thức pre() hoặc post() để đăng ký hook trên schema đó. Xem ví dụ dưới đây:

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        useFactory: () => {
          const schema = CatSchema;
          schema.pre('save', function () {
            console.log('Hello from pre save');
          });
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

Giống như các factory provider khác, hàm factory của chúng ta có thể là async và có thể inject dependency thông qua inject.

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        imports: [ConfigModule],
        useFactory: (configService: ConfigService) => {
          const schema = CatSchema;
          schema.pre('save', function() {
            console.log(
              `${configService.get('APP_NAME')}: Hello from pre save`,
            );
          });
          return schema;
        },
        inject: [ConfigService],
      },
    ]),
  ],
})
export class AppModule {}

Plugin

Để đăng ký một plugin cho một schema cụ thể, dùng phương thức forFeatureAsync().

@Module({
  imports: [
    MongooseModule.forFeatureAsync([
      {
        name: Cat.name,
        useFactory: () => {
          const schema = CatSchema;
          schema.plugin(require('mongoose-autopopulate'));
          return schema;
        },
      },
    ]),
  ],
})
export class AppModule {}

Để đăng ký một plugin cho tất cả schema cùng lúc, gọi phương thức .plugin() của object Connection. Bạn nên truy cập connection trước khi các model được tạo; để làm điều này, dùng connectionFactory:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/test', {
      connectionFactory: (connection) => {
        connection.plugin(require('mongoose-autopopulate'));
        return connection;
      }
    }),
  ],
})
export class AppModule {}

Discriminator

Discriminator là một cơ chế kế thừa schema. Chúng cho phép bạn có nhiều model với các schema chồng lấp nhau trên cùng một collection MongoDB bên dưới.

Giả sử bạn muốn theo dõi nhiều loại event khác nhau trong cùng một collection. Mỗi event sẽ có một timestamp.

@Schema({ discriminatorKey: 'kind' })
export class Event {
  @Prop({
    type: String,
    required: true,
    enum: [ClickedLinkEvent.name, SignUpEvent.name],
  })
  kind: string;

  @Prop({ type: Date, required: true })
  time: Date;
}

export const EventSchema = SchemaFactory.createForClass(Event);

Gợi ý Cách mongoose phân biệt các discriminator model khác nhau là thông qua “discriminator key”, mặc định là __t. Mongoose thêm một path kiểu String tên __t vào schema của bạn để theo dõi document đó là instance của discriminator nào. Bạn cũng có thể dùng tùy chọn discriminatorKey để định nghĩa path dùng cho việc phân biệt.

Các instance SignUpEvent và ClickedLinkEvent sẽ được lưu trong cùng collection với các event chung.

Bây giờ, hãy định nghĩa class ClickedLinkEvent, như sau:

@Schema()
export class ClickedLinkEvent {
  kind: string;
  time: Date;

  @Prop({ type: String, required: true })
  url: string;
}

export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent);

Và class SignUpEvent:

@Schema()
export class SignUpEvent {
  kind: string;
  time: Date;

  @Prop({ type: String, required: true })
  user: string;
}

export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent);

Với các thành phần này, dùng tùy chọn discriminators để đăng ký một discriminator cho một schema cụ thể. Nó hoạt động với cả MongooseModule.forFeature và MongooseModule.forFeatureAsync:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: Event.name,
        schema: EventSchema,
        discriminators: [
          { name: ClickedLinkEvent.name, schema: ClickedLinkEventSchema },
          { name: SignUpEvent.name, schema: SignUpEventSchema },
        ],
      },
    ]),
  ]
})
export class EventsModule {}

Testing

Khi viết unit test cho một ứng dụng, chúng ta thường muốn tránh mọi kết nối database, để test suite đơn giản hơn khi thiết lập và chạy nhanh hơn. Nhưng các class của chúng ta có thể phụ thuộc vào model được lấy ra từ connection instance. Làm sao để resolve các class này? Giải pháp là tạo mock model.

Để việc này dễ dàng hơn, package @nestjs/mongoose cung cấp hàm getModelToken() trả về một injection token đã chuẩn bị sẵn dựa trên tên token. Dùng token này, bạn có thể dễ dàng cung cấp một mock implementation bằng bất kỳ kỹ thuật custom provider tiêu chuẩn nào, bao gồm useClass, useValue, và useFactory. Ví dụ:

@Module({
  providers: [
    CatsService,
    {
      provide: getModelToken(Cat.name),
      useValue: catModel,
    },
  ],
})
export class CatsModule {}

Trong ví dụ này, một catModel được hardcode (một object instance) sẽ được cung cấp bất cứ khi nào một consumer inject Model<Cat> bằng decorator @InjectModel().

Ghi chú người dịch Đây chính là kỹ thuật “dependency injection cho phép mock” mà bạn hay thấy khi test service Go bằng interface giả (fake) thay vì gọi database thật, hoặc như mock một Eloquent model trong Laravel bằng Mockery. Điểm khác biệt là ở Nest bạn không cần interface tường minh - chỉ cần biết đúng injection token (getModelToken(Cat.name)) là có thể “đánh tráo” provider thật bằng provider giả trong bộ test.

Cấu hình bất đồng bộ (Async configuration)

Khi bạn cần truyền các option của module một cách bất đồng bộ thay vì tĩnh, dùng phương thức forRootAsync(). Như với hầu hết các dynamic module, Nest cung cấp một số kỹ thuật để xử lý cấu hình bất đồng bộ.

Một kỹ thuật là dùng factory function:

MongooseModule.forRootAsync({
  useFactory: () => ({
    uri: 'mongodb://localhost/nest',
  }),
});

Giống như các factory provider khác, hàm factory của chúng ta có thể là async và có thể inject dependency thông qua inject.

MongooseModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    uri: configService.get<string>('MONGODB_URI'),
  }),
  inject: [ConfigService],
});

Ngoài ra, bạn có thể cấu hình MongooseModule bằng một class thay vì factory, như dưới đây:

MongooseModule.forRootAsync({
  useClass: MongooseConfigService,
});

Cấu trúc trên khởi tạo MongooseConfigService bên trong MongooseModule, dùng nó để tạo object option cần thiết. Lưu ý rằng trong ví dụ này, MongooseConfigService phải implement interface MongooseOptionsFactory, như dưới đây. MongooseModule sẽ gọi phương thức createMongooseOptions() trên instance của class được cung cấp.

@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
  createMongooseOptions(): MongooseModuleOptions {
    return {
      uri: 'mongodb://localhost/nest',
    };
  }
}

Nếu bạn muốn tái sử dụng một options provider đã có sẵn thay vì tạo bản sao riêng bên trong MongooseModule, dùng cú pháp useExisting.

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

Sự kiện connection (Connection events)

Bạn có thể lắng nghe các sự kiện connection của Mongoose bằng tùy chọn cấu hình onConnectionCreate. Điều này cho phép bạn triển khai logic tùy chỉnh mỗi khi một connection được thiết lập. Ví dụ, bạn có thể đăng ký các event listener cho các sự kiện connected, open, disconnected, reconnected, và disconnecting, như minh họa dưới đây:

MongooseModule.forRoot('mongodb://localhost/test', {
  onConnectionCreate: (connection: Connection) => {
    connection.on('connected', () => console.log('connected'));
    connection.on('open', () => console.log('open'));
    connection.on('disconnected', () => console.log('disconnected'));
    connection.on('reconnected', () => console.log('reconnected'));
    connection.on('disconnecting', () => console.log('disconnecting'));

    return connection;
  },
}),

Trong đoạn code này, chúng ta đang thiết lập một connection tới database MongoDB tại mongodb://localhost/test. Tùy chọn onConnectionCreate cho phép bạn thiết lập các event listener cụ thể để theo dõi trạng thái của connection:

  • connected: Kích hoạt khi connection được thiết lập thành công.
  • open: Bắn ra khi connection đã mở hoàn toàn và sẵn sàng cho các thao tác.
  • disconnected: Được gọi khi connection bị mất.
  • reconnected: Được gọi khi connection được thiết lập lại sau khi bị ngắt.
  • disconnecting: Xảy ra khi connection đang trong quá trình đóng.

Bạn cũng có thể tích hợp thuộc tính onConnectionCreate vào cấu hình bất đồng bộ được tạo bằng MongooseModule.forRootAsync():

MongooseModule.forRootAsync({
  useFactory: () => ({
    uri: 'mongodb://localhost/test',
    onConnectionCreate: (connection: Connection) => {
      // Đăng ký event listener ở đây
      return connection;
    },
  }),
}),

Điều này cung cấp một cách linh hoạt để quản lý các sự kiện connection, giúp bạn xử lý các thay đổi về trạng thái connection một cách hiệu quả.

Subdocument

Để lồng subdocument bên trong một document cha, bạn có thể định nghĩa schema của mình như sau:

@Schema()
export class Name {
  @Prop()
  firstName: string;

  @Prop()
  lastName: string;
}

export const NameSchema = SchemaFactory.createForClass(Name);

Sau đó tham chiếu subdocument trong schema cha:

@Schema()
export class Person {
  @Prop(NameSchema)
  name: Name;
}

export const PersonSchema = SchemaFactory.createForClass(Person);

export type PersonDocumentOverride = {
  name: Types.Subdocument<Types.ObjectId> & Name;
};

export type PersonDocument = HydratedDocument<Person, PersonDocumentOverride>;

Nếu bạn muốn chứa nhiều subdocument, bạn có thể dùng một mảng subdocument. Quan trọng là phải override type của property tương ứng:

@Schema()
export class Person {
  @Prop([NameSchema])
  name: Name[];
}

export const PersonSchema = SchemaFactory.createForClass(Person);

export type PersonDocumentOverride = {
  name: Types.DocumentArray<Name>;
};

export type PersonDocument = HydratedDocument<Person, PersonDocumentOverride>;

Virtual

Trong Mongoose, một virtual là một property tồn tại trên document nhưng không được lưu trữ vào MongoDB. Nó không được lưu trong database mà được tính toán động mỗi khi được truy cập. Virtual thường được dùng cho các giá trị dẫn xuất hoặc được tính toán, như kết hợp các field (ví dụ, tạo một property fullName bằng cách ghép firstName và lastName), hoặc để tạo các property phụ thuộc vào dữ liệu đã có trong document.

class Person {
  @Prop()
  firstName: string;

  @Prop()
  lastName: string;

  @Virtual({
    get: function (this: Person) {
      return `${this.firstName} ${this.lastName}`;
    },
  })
  fullName: string;
}

Gợi ý Decorator @Virtual() được import từ package @nestjs/mongoose.

Trong ví dụ này, virtual fullName được dẫn xuất từ firstName và lastName. Dù hoạt động giống như một property bình thường khi được truy cập, nó không bao giờ được lưu vào document MongoDB:

Ghi chú người dịch Virtual gần giống với accessor/computed property mà bạn hay thấy ở Laravel Eloquent (getFullNameAttribute()) hay getter trong PHP/Go struct method - nó chỉ tồn tại ở tầng application, không sinh ra field thật trong document MongoDB. Cạm bẫy hay gặp: nếu bạn query bằng .lean() (trả về plain object thay vì Mongoose document), virtual sẽ không xuất hiện trong kết quả, vì cơ chế getter chỉ hoạt động trên document đã được Mongoose “hydrate”.

Ví dụ

Một ví dụ hoạt động đầy đủ có sẵn ở đây.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.