本篇為「全端網站架構 」中的後端範例及細節。接續前一篇:全端網站設計範例:連結資料庫 。
有了資料庫之後,繼續實做基本的 User CRUD,所謂 CRUD 也就是 Create, Read, Update 和 Delete 的首字母縮寫。
 
Read 首先在有資料的情況,可以先把資料一筆一筆讀出來,最直接的方式就是在 Resolver 裡面對資料庫進行操作。
先把 GraphQL 的 Schema 定義好,在 general.graphql 中加上
1 2 3 4 5 6 7 8 9 type Query {   user(id: ID!): User! } type User {   id: ID!   username: String!   name: String! } 
 
便能在 Resolver resolvers/index.ts 裡面實做,給定傳入的 id,回傳一整個 User Object
1 2 3 4 5 export  default  {  Query : {     user : (_, { id } ) =>  knex ('Users' ).select ('*' ).where ('id' , id),   }, }; 
 
用 GraphQL 定義完 Schema 的好處就是欄位可以直接全選,不用擔心你資料庫存的其它東西(如密碼)被送出去,因為你沒有定義。
Create, Update, Delete 新增、修改和刪除這裡都屬於寫入資料的範疇,放在 Mutation 中,可參考以下的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export  default  {  Mutation : {     addUser : async  (_, { password, ...args }) => {       const  id = (await  knex ('Users' ).insert ({ password : hashPassword (password), ...args }))[0 ];       return  (await  knex.select ('*' ).from ('Users' ).where ('id' , id))[0 ];     },     editUser : async  (_, { id, password, ...args }) => {       if  (password) args = { ...args, password : hashPassword (password) };       if  (Object .keys (args).length  >= 1 ) await  knex ('Users' ).update (args).where ('id' , id);     },     deleteUser : async  (_, { id }) => {       await  knex ('Users' ).delete ().where ('id' , id);     },   }, }; 
 
Search 在對資料的讀取中,讀單一的 instace 相對簡單一些,但一次讀多筆就複雜的多了。因為考慮的不單單是透過 id 取得唯一值,或是失敗這兩種可能而已,還要考慮
Pagination 
Sorting 
Filtering 
 
不只是考慮單一作用的情況,還要考慮複合的狀態,像是同時排序又過濾還要分頁,光寫 SQL 就感覺有點頭大了!這也是為何用 Knex 的原因,Query Builder 可以幫助我們在組合 SQL 以任意順序加上語法。
我們先直接來看完整的 Search Function,一段一段分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 async  search (searchOptions: SearchOptions, options?: { query?: Knex.QueryBuilder } ) {     const  limit = searchOptions.pagination ?.limit  !== undefined  ? searchOptions.pagination ?.limit  : DEFAULT_PAGE_SIZE ;   const  page = searchOptions.pagination ?.page  !== undefined  ? searchOptions.pagination ?.page  : 0 ;   const  { filters, sorter } = searchOptions;      const  query = options?.query  || knex.queryBuilder ();   query.select ('*' ).from (this .tableName );      if  (filters && Object .keys (filters).length  > 0 ) {     for  (const  [filter, value] of  Object .entries (filters)) {       if  (!this .addFilterImpls [filter]) throw  new  Error (`Filter ${filter}  hasn't been implemented` );       this .addFilterImpls [filter](value, query);     }   }      if  (sorter) {     if  (!this .addSorterImpls [sorter.column ]) throw  new  Error (`Sorter ${sorter.column}  hasn't been implemented` );     this .addSorterImpls [sorter.column ](sorter.direction , query);   }      const  countQuery : { count : number  }[] = await  knex.select (knex.raw ('count(*) as count' )).from (query.as ('sub' ));   const  total = countQuery[0 ].count ;   const  list : [] = await  query.limit (limit).offset (limit * page);   return  { list, total }; } 
 
首先是傳入 Function 的變數,非必填的有 options 裡面的 query,這個變數的用途是在 Search 之前就已經有個 QueryBuilder 的 Instance 了,可以傳進來繼續用。
比較重要的是 searchOption,其定義是這樣的
1 2 3 4 5 6 type  Pagintaion  = { limit : number ; page?: number  };type  Filters  = { [keys : string ]: unknown };type  Direction  = 'asc'  | 'desc' ;type  Sorter  = { column : string ; direction : Direction  };type  SearchOptions  = { pagination?: Pagintaion ; filters?: Filters ; sorter?: Sorter  };
 
裡面要傳入的就是分頁、過濾及排序的內容
分頁時要傳 limit 限制每頁的筆數、page 是第幾頁,預設第 0 頁 
過濾時要傳一整個 Object,假設同時要過濾 username 及 name,這個 Object 就會有這兩個 Key,而裡面的 Value 就看是什麼型態的了,可以是字串,也可以是要過濾大小時的數字等等,都是可以擴充的 
排序時要傳入 column 看以誰做參照來排序;以及 direction 方向,看要升還是降。但不像過濾可以有多個,這邊的實做僅限一個排序而已,因為實務上兩個以上的排序其實用不太到 
 
再來細講 Filters 及 Sorters
Filters 由於每個 Filter 要做的事情不同,很難去寫一個 General 的 Function 給每個 Filter 去做。用個例子來說明,現在有兩個 Table:Teachers Table 和 Students Table,長的像這樣
1 2 Teachers (id, name) Students (id, teacher_id, name) 
 
如果我們現在要撈出所有老師,那麼用老師的 name 來過相對簡單,但是要撈出有教某學生的老師,就麻煩一些了,重點是過濾的方式百百種,與其試著寫一個 General 的版本,不如每個 Filter 都獨自寫 SQL 實做。
1 2 3 4 5 6 7 if  (filters && Object .keys (filters).length  > 0 ) {  for  (const  [filter, value] of  Object .entries (filters)) {     if  (!this .addFilterImpls [filter]) throw  new  Error (`Filter ${filter}  hasn't been implemented` );     this .addFilterImpls [filter](value, query);   } } 
 
所以 addFilterImpls 的這個物件存的就是每個 Filter 的實做 Function,如果要過濾用戶姓名,用的就會是這樣的 Function
1 2 3 function  name (value: string , query: Knex.QueryBuilder ) {  query.where ('Users.name' , 'like' , `%${value} %` ); } 
 
Sorter Sorter 和 Filters 類似,我們也採取個案處理的方式去做,雖然相對於 Filters 的高複雜度,Sorter 最常出現也就是針對數字大小、文字做排序,但偶爾會有其它難處理的例外。
例如上述老師和學生 Tables 的例子,延伸一下,如果要撈出老師但依照其學生平均分數來排序,就不得不另外寫 SQL 了。
這邊是基本的 addSorterImpls
1 2 3 function  name (direction: Direction, query: Knex.QueryBuilder ) {  query.orderBy ('Users.name' , direction); }