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 skip và take 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 page và limit 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.



.png)



