GraphQL 之 Filtering, Sorting, Pagination, Projections

GraphQL 的價值就在會依 Filtering, Sorting, Pagination, Projections 自動組織後端查詢程式碼。

引言

GraphQL 的價值就在會依 Filtering(過濾), Sorting(排序), Pagination(分頁), Projections(投影) 自動組織後端查詢程式碼。這幾項功能是不用再加碼就可以實現。

經過數次的迭代,就 Hot Chocolate 來說可以輕鬆的就實現,當然還是有些前題條件的。軟體規範設計的越玄實作就越難。GraphQL 的規範就很玄那是怎麼做到的,這應該歸功於 LINQ 的動態性,Hot Chocolate 就是基於 LINQ IQueryable 介面實現的。

這四個項目的實作都在後端,前端只要會下規範好的指令即可。


Projections(投影) / UseProjection

其中比較特別的是 Projections(投影)。Projections 的作用是優化後端查詢指令,沒有實作並不會影嚮邏輯上的結果。它優化的部份是去除查詢過程中自動排除不需要的欄位,減少不必要的資料流量。在組織 Projections 之前會取該 table 全部的欄位;Projections 之後只取該 table 需要的部份。

範例:若有一 GrqphQL query 我們只要3個欄位: id, name, city。如下:

query {
  user {
    id,
    name,
    addrsss {
      city
    }
  }
}

在 Projections 之前,對映到後端的 DB Query 指令是

SELECT * 
FROM user U
JOIN address A on U.id = A.userId 
-- 若 user 有 100 個欄位 address 有 10 個欄位,在 Projections 之前查詢過程會取全部欄位。
-- 在此例就是取 110 個欄位。在最後的最後 GraphQL 送回只需要的 3 個欄位。 

在 Projections 之後變成

SELECT U.id, U.name, A.city 
FROM user U
JOIN address A ON U.id = A.userId 
-- 在 Projections 後,查詢過程只會取必要的 3 個欄位。

Projection 實務上資料來源若是映射 DB → ORM → LINQ → GrqphQL ,那這個 Projections 作用就很明顯。而若是自訂的資料來源的話意義就不大。

直接映射資料庫

Hot Chocolate 來說有支援數種資料庫 Entiy Framework Core, MongoDB, Neo4J 等,只要組織好就能進行 GraphQL query 不必再寫 query 程式碼。

直接映射資料庫

參考文件

各別的說明請直接參考官方說明。


Filtering / UseFiltering - GraphQL 規範

從練習的範例來看。進一步的再去查文件。

Graph Schema - about filter (後端)

Schema Definition
# 本例混用 filtering, sorting
type Query {
  bookList(where: BookFilterInput, order: [BookSortInput!]): [Book!]!
}

# 重點說明 filter 相關的 shema 內容。
input BookFilterInput {
  and: [BookFilterInput!]
  or: [BookFilterInput!]
  title: StringOperationFilterInput
  author: AuthorFilterInput
}

input StringOperationFilterInput {
  and: [StringOperationFilterInput!]
  or: [StringOperationFilterInput!]
  eq: String
  neq: String
  contains: String
  ncontains: String
  in: [String]
  nin: [String]
  startsWith: String
  nstartsWith: String
  endsWith: String
  nendsWith: String
}

input AuthorFilterInput {
  and: [AuthorFilterInput!]
  or: [AuthorFilterInput!]
  name: StringOperationFilterInput
}

type Book {
  title: String!
  author: Author!
}
[..略...]

GraphQL query - about filter (前端)

# 本例 filtering
query GetBookListFilter {
  bookList(
    where: {
      and:[
	{ title: {contains: "Python"}},
        { author: { name: { eq: "莊泉福"}}}
      ] 
    }
  ) {
    title,
    author {
      name
    }
  }
}

Sorting / UseSorting - GraphQL 規範

從練習的範例來看。進一步的再去查文件。

Graph Schema - about filter (後端)

Schema Definition
type Query {
  bookList(where: BookFilterInput, order: [BookSortInput!]): [Book!]!
}

input BookSortInput {
  title: SortEnumType
  author: AuthorSortInput
}

enum SortEnumType {
  ASC
  DESC
}

input AuthorSortInput {
  name: SortEnumType
}
[..略...]

Graph query - about filter (前端)

# 本例混用 filtering, sorting 
query GetBookListFilter {
  bookList(
    order: {
      title: DESC
    }
    where: {
      title: {contains: "Python"}
    }
  ) {
    title
  }
}

Pagination / UsePaging - GraphQL 規範

從練習的範例來看。進一步的再去查文件。

Graph Schema - about filter (後端)

Schema Definition
type Query {
  booksPage(
    first: Int       # 下一頁參數,筆數
    after: String    # 下一頁參數,endCursor of this page.
    last: Int        # 上一頁參數,筆數
    before: String   # 上一頁參數,startCursor of this page.
  ): BooksPageConnection
}

"""
A connection to a list of items.
"""
type BooksPageConnection {
  pageInfo: PageInfo!
  edges: [BooksPageEdge!]
  nodes: [Book!]
}

"""
Information about pagination in a connection.
"""
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

"""
An edge in a connection.
"""
type BooksPageEdge {
  cursor: String!
  node: Book!
}

Graph query - about filter (前端)

# 第一頁:取6筆
query GetBooksNextPage {
  booksPage(
    first:6,
    after: null
  ) {
    pageInfo {
      hasNextPage,
      hasPreviousPage,
      startCursor,
      endCursor
    }
    nodes {
      title
    }
  }
}

# 下一頁:取6筆
query GetBooksNextPage {
  booksPage(
    first:6,
    after: "NQ=="  # 下一頁參數,endCursor of this page.
  ) {
    pageInfo {
      hasNextPage,
      hasPreviousPage,
      startCursor,
      endCursor
    }
    nodes {
      title
    }
  }
}

# 上一頁:取6筆
query GetBooksPrevPage {
  booksPage(
    last:6,
    before: "Ng=="  #參數,startCursor of this page.
  ) {
    pageInfo {
      hasNextPage,
      hasPreviousPage,
      startCursor,
      endCursor
    }
    nodes {
      title
    }
  }
}


實作環境

平台: .NET6 IDE: Visual Studio 2022 框架: Blazor Server App GraphQL 套件: Hot Chocolate v13.8.1

實作紀錄(關鍵程式碼)

實作上這四種可以同時混用,但需遵守順序。

實作 Filtering, Sorting, Pagination 都是後端的工作。經過數次的迭代,就 Hot Chocolate 來說可以輕鬆的就實現,只要掛上相應的 middleware / attribute 就完成了。

首先安裝套件 HotChocolate.Data

dotnet add package HotChocolate.Data  ## ※HotChocolate.* 相關套件版本需一致。

在 Program.cs 註冊

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
//## for GrqphQL
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddType<ProductQuery>()
    .AddType<BookQuery>()
    .AddMutationType<Mutation>()
    .AddSubscriptionType<Subscription>() // for GrqphQL subscriptions.
    .AddInMemorySubscriptions()
    .AddProjections() //------ enable projection
    .AddFiltering()   //------ enable filter
    .AddSorting();    //------ enable sorting
    // Pagination 不用額外註冊

[...略...]
var app = builder.Build();
[...略...]

//## for GrqphQL
app.MapGraphQL();
app.UseWebSockets(); // for GrqphQL subscriptions.

app.Run();

加掛 Filtering, Sorting, Pagination, Projections

BookQuery.cs
[ExtendObjectType(nameof(Query))]
public class BookQuery
{
  [UsePaging]
  [UseProjection]
  [UseFiltering]
  [UseSorting] //---※這四個順序必需正確。
  public IQueryable<Book> GetBookList() => _books.AsQueryable();

  [UseFirstOrDefault] //---只傳回一筆,仍保有 filter/sorting 能力。
  [UseFiltering]
  [UseSorting] //---也可以只掛想要的功能即可。
  public IQueryable<Book> GetBookList() => _books.AsQueryable();
  
  [UsePaging] //---也可以只掛想要的功能即可。
  public IQueryable<Book> GetBooksPage() => _books.AsQueryable();

  [UseFiltering] //---也可以混合自訂的(條件)參數,注意名稱別衝突!
  public IQueryable<Book> GetBookListX(string title) =>
             _books.Where(c => c.Title.Contains(title)).AsQueryable();
  
  [...略...]
}

完整程式碼

(EOF)

Last updated