来自结构化对象的 Typeorm 动态查询构建器

Posted

技术标签:

【中文标题】来自结构化对象的 Typeorm 动态查询构建器【英文标题】:Typeorm dynamic query builder from structured object 【发布时间】:2019-06-09 02:07:51 【问题描述】:

为了在 graphql 服务器中使用,我定义了一个结构化输入类型,您可以在其中指定许多过滤条件,这些条件与 prisma 的工作方式非常相似:

这允许我在如下查询中提交结构化过滤器:


  users(
    where: 
      OR: [ email:  starts_with: "ja"  ,  email:  ends_with: ".com"  ],
      AND: [ email:  starts_with: "ja"  ,  email:  ends_with: ".com"  ],
      email: contains: "lowe"
    
  ) 
    id
    email
  

在我的解析器中,我通过一个函数来提供 args.where 以解析结构并利用 TypeOrm 的查询构建器将其转换为正确的 sql。整个函数是:

import  Brackets  from "typeorm";

export const filterQuery = (query: any, where: any) => 
  if (!where) 
    return query;
  

  Object.keys(where).forEach(key => 
    if (key === "OR") 
      where[key].map((queryArray: any) => 
        query.orWhere(new Brackets(qb => filterQuery(qb, queryArray)));
      );
     else if (key === "AND") 
      where[key].map((queryArray: any) => 
        query.andWhere(new Brackets(qb => filterQuery(qb, queryArray)));
      );
     else 
      const whereArgs = Object.entries(where);

      whereArgs.map(whereArg => 
        const [fieldName, filters] = whereArg;
        const ops = Object.entries(filters);

        ops.map(parameters => 
          const [operation, value] = parameters;

          switch (operation) 
            case "is": 
              query.andWhere(`$fieldName = :isvalue`,  isvalue: value );
              break;
            
            case "not": 
              query.andWhere(`$fieldName != :notvalue`,  notvalue: value );
              break;
            
            case "in": 
              query.andWhere(`$fieldName IN :invalue`,  invalue: value );
              break;
            
            case "not_in": 
              query.andWhere(`$fieldName NOT IN :notinvalue`, 
                notinvalue: value
              );
              break;
            
            case "lt": 
              query.andWhere(`$fieldName < :ltvalue`,  ltvalue: value );
              break;
            
            case "lte": 
              query.andWhere(`$fieldName <= :ltevalue`,  ltevalue: value );
              break;
            
            case "gt": 
              query.andWhere(`$fieldName > :gtvalue`,  gtvalue: value );
              break;
            
            case "gte": 
              query.andWhere(`$fieldName >= :gtevalue`,  gtevalue: value );
              break;
            
            case "contains": 
              query.andWhere(`$fieldName ILIKE :convalue`, 
                convalue: `%$value%`
              );
              break;
            
            case "not_contains": 
              query.andWhere(`$fieldName NOT ILIKE :notconvalue`, 
                notconvalue: `%$value%`
              );
              break;
            
            case "starts_with": 
              query
                .andWhere(`$fieldName ILIKE :swvalue`)
                .setParameter("swvalue", `$value%`);
              break;
            
            case "not_starts_with": 
              query
                .andWhere(`$fieldName NOT ILIKE :nswvalue`)
                .setParameter("nswvalue", `$value%`);
              break;
            
            case "ends_with": 
              query.andWhere(`$fieldName ILIKE :ewvalue`, 
                ewvalue: `%$value`
              );
              break;
            
            case "not_ends_with": 
              query.andWhere(`$fieldName ILIKE :newvalue`, 
                newvalue: `%$value`
              );
              break;
            
            default: 
              break;
            
          
        );
      );
    
  );

  return query;
;

哪个有效(有点)但没有像我期望的那样嵌套 AND/OR 查询(并且以前在 KNEX 中工作过)。以上函数生成SQL:

SELECT
  "user"."id" AS "user_id",
  "user"."name" AS "user_name",
  "user"."email" AS "user_email",
  "user"."loginToken" AS "user_loginToken",
  "user"."loginTokenExpiry" AS "user_loginTokenExpiry",
  "user"."active" AS "user_active",
  "user"."visible" AS "user_visible",
  "user"."isStaff" AS "user_isStaff",
  "user"."isBilling" AS "user_isBilling",
  "user"."createdAt" AS "user_createdAt",
  "user"."updatedAt" AS "user_updatedAt",
  "user"."version" AS "user_version"
FROM "user" "user"
WHERE (email ILIKE $1)
  AND (email ILIKE $2)
  OR (email ILIKE $3)
  OR (email ILIKE $4)
  AND email ILIKE $5
-- PARAMETERS: ["ja%","%.com","ja%","%.com","%lowe%"]

但我希望看到更多类似的东西:

..... 
WHERE email ILIKE '%low%' 
AND (
    email ILIKE 'ja%' AND email ILIKE '%.com'
) AND (
    email ILIKE 'ja%' OR email ILIKE '%.com'
)

请原谅无意义的重复查询。我只是想说明预期的 NESTED 语句。

如何强制查询构建器函数的 AND/OR 分支按预期正确嵌套?

** 如果有人可以帮助我找出此处的实际打字稿类型,则加分 **

【问题讨论】:

在您的示例查询中,有 id email,您确定这是您得到的,因为它似乎是一个 json,json 应该有 对。还有什么是查询?您正在调用 query.orWhere 谢谢沙达布。它不是 json,它是一个标准的 Graphql 查询。 id 和 email 代表我想从查询中返回的返回字段。 【参考方案1】:
    将其拆分为 2 个函数,以便更轻松地添加类型 在您的案例陈述中,您需要做 orWhere 或 andWhere 不要在括号上映射,而是将其提升一层
import  Brackets, WhereExpression, SelectQueryBuilder  from "typeorm";

interface FieldOptions 
  starts_with?: string;
  ends_with?: string;
  contains?: string;


interface Fields 
  email?: FieldOptions;


interface Where extends Fields 
  OR?: Fields[];
  AND?: Fields[];


const handleArgs = (
  query: WhereExpression,
  where: Where,
  andOr: "andWhere" | "orWhere"
) => 
  const whereArgs = Object.entries(where);

  whereArgs.map(whereArg => 
    const [fieldName, filters] = whereArg;
    const ops = Object.entries(filters);

    ops.map(parameters => 
      const [operation, value] = parameters;

      switch (operation) 
        case "is": 
          query[andOr](`$fieldName = :isvalue`,  isvalue: value );
          break;
        
        case "not": 
          query[andOr](`$fieldName != :notvalue`,  notvalue: value );
          break;
        
        case "in": 
          query[andOr](`$fieldName IN :invalue`,  invalue: value );
          break;
        
        case "not_in": 
          query[andOr](`$fieldName NOT IN :notinvalue`, 
            notinvalue: value
          );
          break;
        
        case "lt": 
          query[andOr](`$fieldName < :ltvalue`,  ltvalue: value );
          break;
        
        case "lte": 
          query[andOr](`$fieldName <= :ltevalue`,  ltevalue: value );
          break;
        
        case "gt": 
          query[andOr](`$fieldName > :gtvalue`,  gtvalue: value );
          break;
        
        case "gte": 
          query[andOr](`$fieldName >= :gtevalue`,  gtevalue: value );
          break;
        
        case "contains": 
          query[andOr](`$fieldName ILIKE :convalue`, 
            convalue: `%$value%`
          );
          break;
        
        case "not_contains": 
          query[andOr](`$fieldName NOT ILIKE :notconvalue`, 
            notconvalue: `%$value%`
          );
          break;
        
        case "starts_with": 
          query[andOr](`$fieldName ILIKE :swvalue`, 
            swvalue: `$value%`
          );
          break;
        
        case "not_starts_with": 
          query[andOr](`$fieldName NOT ILIKE :nswvalue`, 
            nswvalue: `$value%`
          );
          break;
        
        case "ends_with": 
          query[andOr](`$fieldName ILIKE :ewvalue`, 
            ewvalue: `%$value`
          );
          break;
        
        case "not_ends_with": 
          query[andOr](`$fieldName ILIKE :newvalue`, 
            newvalue: `%$value`
          );
          break;
        
        default: 
          break;
        
      
    );
  );

  return query;
;

export const filterQuery = <T>(query: SelectQueryBuilder<T>, where: Where) => 
  if (!where) 
    return query;
  

  Object.keys(where).forEach(key => 
    if (key === "OR") 
      query.andWhere(
        new Brackets(qb =>
          where[key]!.map(queryArray => 
            handleArgs(qb, queryArray, "orWhere");
          )
        )
      );
     else if (key === "AND") 
      query.andWhere(
        new Brackets(qb =>
          where[key]!.map(queryArray => 
            handleArgs(qb, queryArray, "andWhere");
          )
        )
      );
    
  );

  return query;
;

【讨论】:

这太棒了。谢谢你本。唯一不包括的情况是根级别语句未嵌套在 AND 或 WHERE 下。在我上面的示例查询中,email: contains: "lowe" 被忽略。在您看来,我应该接受根级别的 where 语句还是我应该要求所有语句嵌套在 OR 或 AND 中? ``` 现在,随着我不断改进,仅将 AND/OR 作为根级元素开始变得更有意义。这种方式更加明确,也没有太多不便。 如果您确实想在根级别添加它,您可以添加一个 else 来调用函数 else handleArgs(query, where, "andWhere") 这假设没有字段有重复的名称,但是,如何解决两个表连接并且函数必须区分第一个实体中的 id 和第二个实体中的 id 的情况没有出现错误的实体:Error: ER_NON_UNIQ_ERROR: Column 'id' in where clause is ambiguous? @benawad 对不起,你是the ben awad吗?【参考方案2】:

根据 Ben 的回答,我对函数进行了一些调整,以允许使用更通用的“filter”对象:

// enum
export enum Operator 
  AND = 'AND',
  OR = 'OR',


// interfaces
interface FieldOptions 
  is?: string;
  not?: string;
  in?: string;
  not_in?: string;
  lt?: string;
  lte?: string;
  gt?: string;
  gte?: string;
  contains?: string;
  not_contains?: string;
  starts_with?: string;
  not_starts_with?: string;
  ends_with?: string;
  not_ends_with?: string;


export interface Field 
  [key: string]: FieldOptions;


export type Where = 
  [K in Operator]?: (Where | Field)[];
;

// functions
export const filterQuery = <T>(query: SelectQueryBuilder<T>, where: Where) => 
  if (!where) 
    return query;
   else 
    return traverseTree(query, where) as SelectQueryBuilder<T>;
  
;

const traverseTree = (query: WhereExpression, where: Where, upperOperator = Operator.AND) => 
  Object.keys(where).forEach((key) => 
    if (key === Operator.OR) 
      query = query.orWhere(buildNewBrackets(where, Operator.OR));
     else if (key === Operator.AND) 
      query = query.andWhere(buildNewBrackets(where, Operator.AND));
     else 
      // Field
      query = handleArgs(query, where as Field, upperOperator === Operator.AND ? 'andWhere' : 'orWhere');
    
  );

  return query;
;

const buildNewBrackets = (where: Where, operator: Operator) => 
  return new Brackets((qb) =>
    where[operator].map((queryArray) => 
      traverseTree(qb, queryArray, operator);
    ),
  );
;

const handleArgs = (query: WhereExpression, field: Field, andOr: 'andWhere' | 'orWhere') => 
  ...
;

这样我们现在可以将这种对象作为查询参数:


  AND: [
    
      OR: [
        
          name: 
            is: 'John'
          ,
        ,
        
          surname: 
            is: 'Doe'
          ,
        
      ]
    ,
    
      AND: [
        
          age: 
            gt: 30
          ,
        ,
        
          type: 
            not: 'Employee'
          
        
      ]
    ,
    
      registered_date: 
        gte: '2000-01-01'
      
    
  ]

生成的查询将是:

SELECT *
FROM users U 
WHERE (U.name = 'John' OR U.surname = 'Doe') AND (U.age > 30 AND U.type != 'Employee') AND U.registered_date >= '2000-01-01';

【讨论】:

以上是关于来自结构化对象的 Typeorm 动态查询构建器的主要内容,如果未能解决你的问题,请参考以下文章

NestJS 中的 TypeORM - 如何在实体中获取 MySQL 嵌套对象并对子关系进行“位置”查询?

TypeORM select with case insensitive distinct

如何修复预期值:“1”收到的数组:带有jest框架的TypeORM中的[{“COUNT(*)”:“1”}]错误

TypeORM 是不是支持输入和输出的原始 SQL 查询?

查询中的 TypeORM、反引号与冒号

动态 Linq 查询 - 如何构建 select 子句?