对于 TypeORM 精准&模糊查询的简单二次封装

文章目录

  • [对于 Typeorm 模糊查询的简单二次封装](#对于 Typeorm 模糊查询的简单二次封装)
    • 代码
    • 使用实例
      • [定义一个 filterDto](#定义一个 filterDto)
      • [Service 中使用](#Service 中使用)

对于 Typeorm 模糊查询的简单二次封装

笔者目前在做 NestJS + TyoeORM 项目,需要对查询逻辑进行二次封装,以适应多变的查询需求。本文将通过一个具体的示例------addQueryFilterConditions 函数,探讨如何在 TypeORM 中实现精确匹配和模糊查询的灵活结合,从而提升查询的灵活性和可维护性。

在一般项目开发中,数据过滤条件通常由前端传递给后端,而后端需要根据这些条件动态生成 SQL 查询语句。addQueryFilterConditions 函数正是为此目的而生,它接收查询构造器 (SelectQueryBuilder)、别名 (alias)、过滤数据传输对象 (filterDto) 以及查询选项 (options),根据这些参数动态添加 WHERE 条件到查询中,支持精确匹配和模糊查询两种模式。模糊查询不区分大小写。

此函数的核心在于它能够根据传入的 options 自动判断哪些字段需要进行精确匹配 (equal),哪些需要模糊匹配 (like)。例如,对于精确匹配,它会生成形如 ${alias}.${key} = :${key} 的条件;而对于模糊匹配,则生成 ${alias}.${key} LIKE LOWER(:${key}) 的条件,其中 % 符号用于实现模糊搜索。

如果 filterDto 中的 key 既不在 equal 也不在 like 里面,会被函数自动忽略,这样做的目的是,有些字段需要用更复杂的查询语句,这些查询需求不在此函数的开发目的中。

addQueryFilterConditions 函数并不直接返回任何值。其原因在于该函数的设计目的是直接操作传递给它的 SelectQueryBuilder 实例,通过.andWhere() 方法动态添加 WHERE 条件到查询构造器上,进而影响后续的数据库查询操作。

这样做的好处有:

  1. 链式调用的便利性 :TypeORM 的查询构建鼓励链式调用风格,addQueryFilterConditions 函数通过修改传入的 queryBuilder 对象,使得调用者可以在同一个链中继续添加其他查询条件或执行查询,保持代码的流畅性。
  2. 避免不必要的数据复制:由于函数直接操作原对象,避免了因返回新的查询构造器实例而导致的潜在性能开销或资源占用。
  3. 提升代码的可维护性和可读性:函数无返回值的设计使得调用者清楚地知道,此函数的作用是修改查询构造器的状态而非生成新的数据结构,这有助于理解函数的副作用和使用场景。
  4. 便于复用与扩展:不依赖返回值的设计使得该函数可以更容易地融入到不同的查询逻辑中,无论是单独使用还是与其他查询构建逻辑组合,都能保持良好的兼容性和灵活性。

代码

typescript 复制代码
import { isEmpty } from "radash"; // Radash 判空函数,当为 []、{}、""、null、undefined 的时候会返回 true
import { ObjectLiteral, type SelectQueryBuilder } from "typeorm";

interface IFilterOptions {
  equal?: string[];
  like?: string[];
}

/**
 * 为查询构造器添加过滤条件。
 * 此函数根据提供的过滤数据和选项动态地向查询构建器添加等值和模糊匹配条件。
 *
 * @param {string} alias 实体别名,在查询构造器中使用的表或实体别名。
 * @param {SelectQueryBuilder<any>} query 查询构造器实例,用于构建SQL查询。
 * @param {Record<string, any>} filterDto 过滤数据传输对象,包含需过滤的键值对。
 * @param {IFilterOptions} options 过滤选项,定义了哪些键应用等值匹配 (`equal`) 和哪些键应用模糊匹配 (`like`)。
 */
export const addQueryFilterConditions = (
  alias: string,
  query: SelectQueryBuilder<any>,
  filterDto: Record<string, any>,
  options: IFilterOptions
) => {
  console.log("run!");
  const willProcessKeys = [
    ...(options.equal || []),
    ...(options.like || []),
  ];

  willProcessKeys.forEach((key: string) => {
    if (options.equal?.includes(key)) {
      const value = filterDto[key];
      if (!isEmpty(value)) {
        console.log("2nd", key, value);
        query.andWhere(`${alias}.${key} = :${key}`, {
          [key]: value
        });
      }
    }

    if (options.like?.includes(key)) {
      const value = filterDto[key];
      console.log("3rd", key, value);
      if (!isEmpty(value)) {
        query.andWhere(`${alias}.${key} LIKE LOWER(:${key})`, {
          [key]: `%${value.toLowerCase()}%`
        });
      }
    }
  });
};

使用实例

定义一个 filterDto

typescript 复制代码
export class UserFilterDto {
  id?: string;
  userName?: string;
  status?: string;
}

Service 中使用

假设前端传来的 Body 是:

json 复制代码
{
  "id": "root",
  "userName": ""
}
typescript 复制代码
export class UserService {
  async findList(
    filterDto: UserFilterDto,
    queryParams: IBackendPaginatedQueryParamsWithMode
  ) {
    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");

    // 根据 userId 和 userName 模糊查询
    addQueryFilterConditions("user", queryBuilder, filterDto, {
      like: ["id", "userName"],
    });

    return queryBuilder.getMany();

    // `setQueryBuilderPagination`的回调函数,处理 roleIds 数组
    // const processQueryRecords = (records: Record<string, any>[]) => {
    //   return records.map((item: Record<string, any>) => {
    //     item.roleIds = item.roles.map((item) => item.id);
    //     return item;
    //   });
    // };

    // 分页公共方法,根据 queryBuilder、分页参数、回调函数。
    // return await setQueryBuilderPagination<User>(
    //   queryBuilder,
    //   queryParams,
    //   processQueryRecords
    // );
  }
}

那么 addQueryFilterConditions 就会根据 id = "root" 值来进行模糊查询,userName 值为空字符串,会自动跳过。

相关推荐
前端小盆友2 小时前
从零实现一个GPT 【React + Express】--- 【5】实现网页生成能力
gpt·react.js·express
搞前端的小菜8 小时前
从零实现一个GPT 【React + Express】--- 【2】实现对话流和停止生成
gpt·react.js·express
鲸鱼14666570754198 小时前
Screeps TypeScript 教程:使用 tsup 解决模块加载问题并实现自动化部署
typescript
Q_Q5110082859 小时前
python的保险业务管理与数据分析系统
开发语言·spring boot·python·django·flask·node.js·php
摘星小杨20 小时前
安装nvm管理node.js,详细安装使用教程和详细命令
node.js·nvm
灋✘逞_兇1 天前
Node.Js是什么?
服务器·javascript·node.js
张志鹏PHP全栈1 天前
TypeScript 第四天,TypeScript的编译选项(一)
前端·typescript
Toomey1 天前
别再用 Parameters 乱推断了!vue-i18n 封装 t 函数的正确姿势
typescript
归于尽1 天前
回调函数在Node.js中是怎么执行的?
前端·javascript·node.js
GDAL1 天前
多字节字符的字节被拆分到不同 chunk 中,导致解码失败
node.js