对于 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
    );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luke Paul Na

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值