对于 NestJS + TypeORM 查询构造器分页功能的简单二次封装
NestJS 作为 Node.js 领域备受欢迎的框架,其与 TypeORM 的结合为开发者提供了强大的 ORM 能力,简化了数据库操作。然而,在处理分页查询时,直接在每个服务方法中重复编写分页逻辑既不高效也容易出错。为此,我们可以通过创建一个通用的分页处理函数,将分页逻辑从业务逻辑中抽离出来,达到代码复用和模块化的目的。
本文将介绍如何对 TypeORM 的查询构造器(QueryBuilder)进行简单的二次封装,以便于我们在 API 层快速实现分页功能,同时保持代码的整洁与可维护性。我们将通过一个具体示例来演示这一过程,即通过封装一个分页查询方法,来查询用户列表并进行相应的数据处理。
我们的目标是设计一个函数setQueryBuilderPagination
,它接受一个查询构造器实例、分页参数以及一个可选的回调函数作为参数,返回一个包含分页信息的对象。这个函数将负责处理查询构造器的分页逻辑,如设置每页数量、跳过行数等,同时也支持通过回调函数对查询结果进行后处理。
功能实现
export interface IBackendPaginatedQueryParams {currentPage: number;pageSize: number;
}interface IBackendPaginatedList {currentPage: number;totalPages: number;total: number;pageSize: number;records: any[];
}/*** 为查询构造器设置分页功能。* @async* @template T 泛型类型,继承自 ObjectLiteral。* @param {SelectQueryBuilder<T>} queryBuilder 查询构造器实例。* @param {IBackendPaginatedQueryParams} paginationQueryParams 分页查询参数。* @param {((records: T[]) => T[])?} [callback] 可选的回调函数,用于处理记录数据。* @returns {Promise<IBackendPaginatedList>} 包含当前页、总页数、总数和记录数据的分页信息对象。*/
export const setQueryBuilderPagination = async <T extends ObjectLiteral>(queryBuilder: SelectQueryBuilder<T>,paginationQueryParams: IBackendPaginatedQueryParams,callback?: (records: Record<string, any>[]) => Record<string, any>[]
): Promise<IBackendPaginatedList> => {const { pageSize, currentPage } = paginationQueryParams;queryBuilder.take(pageSize).skip((currentPage - 1) * pageSize);const [recordsRaw, total] = await queryBuilder.getManyAndCount();const records = callback ? callback(recordsRaw) : recordsRaw;return {currentPage: Number(currentPage),totalPages: Math.ceil(total / pageSize),total: Number(total),pageSize: Number(pageSize),records,};
};
核心逻辑讲解
首先,封装的核心在于如何处理分页参数。在setQueryBuilderPagination
函数中,我们接收两个主要参数:查询构造器实例queryBuilder
和分页查询参数paginationQueryParams
。分页参数包括每页显示的记录数pageSize
和当前请求的页码currentPage
。函数内部首先读取这两个参数,然后利用它们设置查询构造器的分页行为:通过.take(pageSize)
限制查询结果的数量,通过.skip((currentPage - 1) * pageSize)
确定从哪条记录开始获取数据。这样就完成了基本的分页设置。
数据获取与计数
紧接着,通过调用.getManyAndCount()
方法,我们一步完成数据的获取及总记录数的统计。这一步骤至关重要,因为它既高效地获取了当前页面的数据,又提供了计算总页数所需的信息。返回的结果是一个数组,其中第一个元素是查询结果,第二个元素是总记录数。
后处理回调
为了增强灵活性,我们引入了一个可选的回调函数callback
,它允许在返回最终结果前对原始查询数据进行进一步加工。例如,我们可以利用这个回调来格式化数据、添加额外属性或进行数据筛选。在示例中,processQueryRecords
函数就是一个典型的后处理示例,它遍历查询结果,为每个用户添加一个roleIds
属性,该属性包含了用户所有角色的 ID。
返回分页信息对象
最后,我们将分页后的数据整理成统一格式返回给调用者。这个格式通常包含当前页码、总页数、总记录数、每页大小以及具体的记录数据。这样的返回结构对于前端分页展示非常友好,易于解析和处理。
应用实例:用户列表查询
接下来,我们将展示如何在实际业务场景中应用此封装函数。假设我们有一个需求,要查询用户列表,同时需要对查询结果中的角色进行处理,提取每个用户的角色 ID 数组。
export class UserService {constructor(private roleService: RoleService,@InjectRepository(User) private readonly userRepository: Repository<User>) {}async findList(filterDto: UserFilterDto,queryParams: IBackendPaginatedQueryParams) {const queryBuilder = this.userRepository.createQueryBuilder("user").select(["user.id","user.userName","user.email","user.createdTime","user.updatedTime","user.status","user.remark",]).leftJoinAndSelect("user.roles", "roles");const processQueryRecords = (records: Record<string, any>[]) => {return records.map((item: Record<string, any>) => {item.roleIds = item.roles.map((item) => item.id);return item;});};return await setQueryBuilderPagination<User>(queryBuilder,queryParams,processQueryRecords);}
}