タイプスクリプトとタイプを使用したGraphSQLクエリのフィルタリング


この記事では、GraphSQL APIに加えることのできるフィルタリング機能について説明します.場合によっては、1つまたは複数のマッチングルールまたは様々なルールの構成を適用することによってデータをフィルタリングする必要があります.
このアイデアは、我々のGraphSQL APIを通してORMのような問い合わせ能力を公開することです.そこでは、クライアント側で柔軟性を持ち、複雑なデータを求めることができます.
リレーショナルデータベースのテーブルの例を次に示します

顧客


ID
名称
シティ
事後コード
11
ハリーアッシュワース
ロンドン
EC 2 5 NT
12
パトリシシ・シンプソン
ブエノスアイレス
51010
13
ビクトリアチャン
ロンドン
N 6 4 AL

注文


ID
カスタマーキューID
オルデナリ

製品
10289
11
2016 - 08 - 26
30
おもちゃ
10290
11
2016 - 08 - 27
25
プログラミング書籍
10410年
12
2017 - 01 - 10
49
プログラミング書籍
10411
12
2017 - 03 - 15
34
フィクション
10259
13
2016 - 07 - 18
10
おもちゃ

エンティティクラス


TypeORM は様々なJavaScriptプラットフォーム(例えばnode . js)で実行できるORMです.これらは、上記で紹介されているテーブルにマッチするタイプのエンティティクラスです.
@Entity("Customers")
export class CustomerEntity {
  @PrimaryGeneratedColumn({ type: "int", name: "id" })
  id: number;

  @Column("varchar", { name: "name", length: 255 })
  name: string;

  @Column("varchar", { name: "city", length: 255 })
  city: string;

  @Column("varchar", { name: "postal_code", length: 255 })
  postalCode: string;

  @OneToMany(() => OrderEntity, order => order.customer)
  orders: OrderEntity[];
}


@Entity("Orders")
export class OrderEntity {
  @PrimaryGeneratedColumn({ type: "int", name: "id" })
  id: number;

  @Column("datetime", { name: "order_date" })
  orderDate: Date;

  @Column("int", { name: "quantity" })
  quantity: number;

  @Column("varchar", { name: "product", length: 255 })
  product: string;

  @ManyToOne(() => CustomerEntity, customer => customer.orders)
  @JoinColumn({name: 'customer_id', referencedColumnName: 'id'})
  customer: CustomerEntity;
}
各クラスの末尾の追加リレーションフィールドに注意してください.これら二つのテーブルの関係を説明します.私たちのケースでは、顧客と注文の間の多くの関係に1つです.だから顧客は多くの注文を持つことができます.

クエリフィルタ


さて、クライアント側が上記のデータについて複雑な質問をする必要があるとしましょう.例えば以下のようになります.Get all orders that:
(have quantity >= 20) AND (ordered books) AND [
   (order date >= '2016-08-27') OR (customers with ids 11, 12) OR   (customers whose postal code contains '5NT')
]
この複雑なクエリをSQL形式でアトミックフィルタに分割できます.
a = (OrderEntity.quantity >= 20)
b = (OrderEntity.product LIKE '%books%')
c = (OrderEntity.orderDate >= '2016-08-27')
d = (CustomerEntity.id IN (11, 12))
e = (CustomerEntity.postalCode LIKE '%5NT%')
論理式は次のようになります.
a AND b AND (c OR d OR e)
この論理式は式ツリーとして表現できます:

見るhere 式ツリーの詳細については

グラフィカルなスキーマ


では、このようなクエリを実行できるようにするためのGraphSQLスキーマを作成しましょう.
    enum Operator {
      AND
      OR
    }

    enum Operation {
      EQ
      IN
      LIKE
      GE
    }

    input Filter {
      op: Operation!
      values: [String!]!
      field: String!
      relationField: String
    }

    input FiltersExpression {
      operator: Operator!
      filters: [Filter!]
      childExpressions: [FiltersExpression!]
    }
メインタイプはこちらFiltersExpression 式ツリーの非葉節に対応します.これらは' AND 'または' OR 'のような論理演算ノードです.
葉節はFilter 種類これらは、私たちが小文字(A、B、C、D、E)と名付けた原子フィルターです.

フィルタ式
  • operator - 論理演算子('、' OR ')
  • filters - このノードの子孫原子フィルター(葉節).
  • childExpressions - 子孫のサブ式.
  • *指定した論理式を構築するときFiltersExpression ノードoperator を返します.

    フィルタ
    原子フィルター用のGraphSQL型(式ツリーの葉ノード).例えば、CustomerEntity.id IN (11, 12) .
  • op - 原子フィルターの条件付き操作op: IN ).
  • values - フィルタの値values: 11, 12 ).
  • field - テーブルのフィールド名field: CustomerEntity.id ).
  • relationField - このオプションのパラメータは、メインテーブルの外部キーを表し、field . typeORMでは、このクラスをエンティティークラスに追加して、テーブル間の関係を示します.relationField SQLクエリでこれらのテーブルに参加するために使用されます.(この例ではrelationField: OrderEntity.customer ).
  • それでは、サーバー側のgraphqlスキーマの残りを見てみましょう.はい、Customer and Order typem実体に従った型、およびgetOrders フィルタ式を受け取ったクエリを返します.
        type Customer {
          id: Int!
          name: String!
          city: String
          postalCode: String!
        }
    
        type Order {
          id: Int!
          customer: Customer!
          orderDate: String!
          quantity: Int!
          product: String!
        }
    
        extend type Query {
          getOrders(filters: FiltersExpression): [Order!]!
        }
    

    クライアント側


    クライアント側では、私たちはgetOrders 我々の主な例からその複雑なフィルタ式を使用している質問.クライアントが送信したGraphSQLクエリの本文です.
    query getOrders {
      getOrders(filters: {
        operator: AND
        filters: [
          {
            field: "OrderEntity.quantity"
            op: GE
            values: ["20"]
          },
          {
            field: "OrderEntity.product"
            op: LIKE
            values: ["books"]
          }
        ]
        childExpressions: [
          {
            operator: OR        
            filters: [
              {
                field: "OrderEntity.orderDate"
                op: GE
                values: ["2016-08-27"]
              },
              {
                field: "CustomerEntity.id"
                relationField: "OrderEntity.customer"
                op: IN
                values: ["11", "12"]
              },
              {
                field: "CustomerEntity.postalCode"
                relationField: "OrderEntity.customer"
                op: LIKE
                values: ["5NT"]
              }
            ]
          }
        ]
      }) {
        id
        orderDate
        quantity
        product
        customer {
          name
          city
        }
      }
    }
    
    

    サーバ側


    さて、このフィルタリングされたクエリを実装する準備が整いました.してください.getOrders :

    OrderResolver.TS
    import {getRepository} from 'typeorm';
    import {SelectQueryBuilder} from 'typeorm/query-builder/SelectQueryBuilder';
    import {OrderEntity} from './OrderEntity';
    
    export const resolvers = {
      Query: {
        getOrders: (parent, {filters}): Promise<OrderEntity[]> => {
            const ordersRepo = getRepository(OrderEntity);
            const fqb = new FilterQueryBuilder<OrderEntity>(ordersRepo, filters);
            const qb: SelectQueryBuilder = fqb.build();
    
            return qb.getMany();
        }
    }
    
    リゾルバはFilterQueryBuilder typeRMクエリを構築するにはそれをつくりましょう.

    FilterQueryBuilder.TS
    import {Repository} from 'typeorm';
    import {SelectQueryBuilder} from 'typeorm/query-builder/SelectQueryBuilder';
    
    export default class FilterQueryBuilder<Entity> {
      private readonly qb: SelectQueryBuilder<Entity>;
    
      constructor(entityRepository: Repository<Entity>,
                  private filtersExpression?: FiltersExpression) {
        this.qb = entityRepository.createQueryBuilder();
      }
    
      build() {
        const jb = new JoinBuilder<Entity>(this.qb, this.filtersExpression);
        jb.build();
    
        const wb = new WhereBuilder<Entity>(this.qb, this.filtersExpression);
        wb.build();
    
        return this.qb;
      }
    }
    
    JoinBuilder FiltersExpressionを再帰的に横切って、それぞれのために左の結合を加えますrelationField .

    ジョイントビルダー.TS
    import {forEach} from 'lodash';
    import {SelectQueryBuilder} from 'typeorm/query-builder/SelectQueryBuilder';
    
    class JoinBuilder<Entity> {
      private joinedEntities = new Set<string>();
    
      constructor(private readonly qb: SelectQueryBuilder<Entity>,
                  private filtersExpression?: FiltersExpression) {
      };
    
      build() {
        if (this.filtersExpression)
          this.buildJoinEntitiesRec(this.filtersExpression);
      }
    
      private buildJoinEntitiesRec(fe: FiltersExpression) {
        forEach(fe.filters, f => this.addJoinEntity(f.field, f.relationField));
        forEach(fe.childExpressions, child => this.buildJoinEntitiesRec(child));
      }
    
      private addJoinEntity(field: string, relationField?: string) {
        const entityName = field.split('.')[0];
    
        if (relationField && !this.joinedEntities.has(entityName)) {
          this.qb.leftJoinAndSelect(relationField, entityName);
          this.joinedEntities.add(entityName);
        }
      }
    }
    
    WhereBuilder 再帰的にフィルタ式ツリーを越え、SQLクエリのWHERE句を構築します.

    WhereBuilderTS
    import { isEmpty, map } from 'lodash';
    import {SelectQueryBuilder} from 'typeorm/query-builder/SelectQueryBuilder';
    
    type ParamValue = string | number | Array<string|number>;
    
    
    export default class WhereBuilder<Entity> {
      private params: Record<string, ParamValue> = {};
      private paramsCount = 0;
    
      constructor(private readonly qb: SelectQueryBuilder<Entity>,
                  private filtersExpression?: FiltersExpression) {
      };
    
      build() {
        if (!this.filtersExpression)
          return;
    
        const whereSql = this.buildExpressionRec(this.filtersExpression);
        this.qb.where(whereSql, this.params);
      }
    
      private buildExpressionRec(fe: FiltersExpression): string {
        const filters = map(fe.filters, f => this.buildFilter(f));
        const children = map(fe.childExpressions, child => this.buildExpressionRec(child));
    
        const allSqlBlocks = [...filters, ...children];
        const sqLExpr = allSqlBlocks.join(` ${fe.operator} `);
        return isEmpty(sqLExpr) ? '' : `(${sqLExpr})`;
      }
    
      private buildFilter(filter: Filter): string {
        const paramName = `${filter.field}_${++this.paramsCount}`;
    
        switch (filter.op) {
          case 'EQ':
            this.params[paramName] = filter.values[0];
            return `${filter.field} = :${paramName}`;
          case 'IN':
            this.params[paramName] = filter.values;
            return `${filter.field} IN (:${paramName})`;
          case 'LIKE':
            this.params[paramName] = `%${filter.values[0]}%`;
            return `${filter.field} LIKE :${paramName}`;
          case 'GE':
            this.params[paramName] = filter.values[0];
            return `${filter.field} >= :${paramName}`;
          default:
            throw new Error(`Unknown filter operation: ${filter.op}`);
        }
      }
    }
    

    最後に、これはFilterQueryBuilder 以下に例を示します:
    SELECT *
    FROM Orders o, 
    LEFT JOIN Customers c ON o.customer_id = c.id
    WHERE (o.quantity >= 20) AND (o.product LIKE '%books%')
          AND 
          (
            (o.order_date >= '2016-08-27') OR 
            (c.id IN (11, 12)) OR 
            (c.postal_code LIKE '%5NT%')
          )
    

    結論


    このようなGraphSQL APIは、クライアントアプリケーションのための大きな柔軟性と制御を提供します.これらはすべて、安全性と実行時の検証を維持している間、GraphSQLからボックスから取得します.
    あなたのデータをフィルタリングするルールの異なる組み合わせでは、正確にあなたが興味を持っているデータを表現することができますし、あなたのためにそれを取得するバックエンドを聞かせてください.
    同様に、我々はさらにそれを強化する私たちのクエリにソートとページ付けを追加することもできます.