本篇為「全端網站架構 」中的後端範例及細節。接續前一篇:全端網站設計範例:連結資料庫 。
有了資料庫之後,繼續實做基本的 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); }