Giới thiệu vấn đề

Phân trang thường gây nghẽn hiệu năng khi truy vấn bảng lớn. TypeORM mặc định trả về toàn bộ bản ghi, gây tiêu tốn bộ nhớ và thời gian. Giải pháp: dùng skiptake kết hợp orderBy để giảm tải.

Cấu hình Entity

Đảm bảo entity có chỉ mục trên cột sắp xếp, thường là createdAt hoặc id.

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

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

  @Column()
  @Index()
  title: string;

  @Column('text')
  content: string;

  @CreateDateColumn()
  @Index()
  createdAt: Date;
}

Service thực hiện phân trang

Sử dụng Repository để xây dựng truy vấn. Đặt skip = (page-1)*limit, take = limit. Thêm order để tránh kết quả không ổn định.

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

@Injectable()
export class ArticleService {
  constructor(@InjectRepository(Article) private repo: Repository
) {} async paginate(page: number = 1, limit: number = 10) { const [items, total] = await this.repo.findAndCount({ skip: (page - 1) * limit, take: limit, order: { createdAt: 'DESC' }, }); return { items, total, page, limit, totalPages: Math.ceil(total / limit), }; } }

Controller trả về dữ liệu

Nhận pagelimit từ query string, truyền cho service.

import { Controller, Get, Query } from '@nestjs/common';
import { ArticleService } from './article.service';

@Controller('articles')
export class ArticleController {
  constructor(private readonly service: ArticleService) {}

  @Get()
  async getArticles(@Query('page') page: string, @Query('limit') limit: string) {
    const pageNum = parseInt(page, 10) || 1;
    const limitNum = parseInt(limit, 10) || 10;
    return this.service.paginate(pageNum, limitNum);
  }
}

Tối ưu thêm với QueryBuilder

Khi cần join hoặc tính toán phức tạp, dùng createQueryBuilder để kiểm soát SQL. Ví dụ, đếm số bình luận cho mỗi bài.

const qb = this.repo.createQueryBuilder('a')
  .leftJoinAndSelect('a.comments', 'c')
  .loadRelationCountAndMap('a.commentCount', 'a.comments')
  .orderBy('a.createdAt', 'DESC')
  .skip((page - 1) * limit)
  .take(limit);
const [items, total] = await qb.getManyAndCount();

Kiểm tra hiệu năng

Dùng EXPLAIN ANALYZE trên PostgreSQL để xác nhận index được sử dụng. Nếu không, tạo index thủ công:

CREATE INDEX idx_article_created_at ON article ("createdAt" DESC);

Kết luận

Áp dụng skip/take, index và QueryBuilder giảm thời gian phản hồi dưới 200ms cho bảng có hàng triệu bản ghi. Tham khảo khóa học "RESTful API với NestJS & TypeORM" tại đây.