Query - 重新思考 Rails 架構
前面我們主要著重在寫入的處理,如果是單純寫入的處理,我們可以使用 Query 來處理。基本上 Command 和 Query 都可以看作 Use Case(使用案例)的一種,可以根據複雜程度決定是否需要細分,當我們實踐到這個階段,也接近有讀寫分離的系統所呈現的狀態。
Query
Query 的實作跟 Command 基本上是相同的,只是我們更明確的將職責「讀取」和「寫入」區分開來,因此只需要在 lib/shipments/query
目錄下增加一個 find_shipment.rb
即可。
1module Shipments
2 module Query
3 class FindShipment
4 attr_reader :shipment_repository
5
6 def initialize(shipment_repository:)
7 @shipment_repository = shipment_repository
8 end
9
10 def execute(shipment_id)
11 shipment_repository.find_shipment(shipment_id.to_i)
12 end
13 end
14 end
15end
我們在「取得 Shipment」的機制上跟 Command 是同樣的邏輯,這表示我們能用相同的 Repository 來處理,因此就不需要重新處理 Repository 的實作,直接沿用現有的行為。
Query 也很適合用於處理先前提到的報表情境,因為我們已經將行為限定在報表相關的處理,因此可以集中精神在報表的查詢上,這種情境時,能在 Repository 直接撰寫 SQL 會比 ORM 更加高效,這也是為什麼 Repository 不直接等同於 Rails 的 ActiveRecord。
測試
撰寫測試是一個好習慣,能確保我們在新增或者修改時不會影響現有的行為,也能更直觀反應每個功能的邏輯。在原本的 features/shipment.feature
可以加入新的描述來反應這次新增的功能。
1Feature: Shipment
2 # ...
3 Scenario: I can get the shipment
4 Given there have some shipment route
5 | route_id | shipment_id | delivered_at |
6 | OKA | 1 | 2024-03-19T00:00:00Z |
7 When I make a GET request to "/shipments/1"
8 Then the response should be
9 """
10 {
11 "id": 1,
12 "state": "shipping",
13 "routes": [
14 {
15 "route_id": "OKA",
16 "shipment_id": 1,
17 "delivered_at": "2024-03-19T00:00:00.000Z"
18 }
19 ]
20 }
21 """
因為有新的步驟,還會需要在 Step Definition 中,針對 GET
的情境增加新的定義。
1When('I make a GET request to {string}') do |path|
2 @response = get path
3end
完善功能
最後,我們就可以把前面沒有提到的部分加入到 Rails 中。先針對 config/routes.rb
調整將預設值設定為 JSON 格式。
1# ...
2 resources :shipments, module: :shipments, defaults: { format: :json } do
3 resources :routes, only: [:create], defaults: { format: :json }
4 end
接著加入 app/shipments/shipments_controller.rb
呼叫這次增加的 Query 物件。
1module Shipments
2 class ShipmentsController < ApplicationController
3 def show
4 query = Shipments::Query::FindShipment.new(shipment_repository: ShipmentRoute)
5 @shipment = query.execute(params[:id])
6 end
7 end
8end
跟增加 View 的方式相同,這次加入 app/shipments/shipments/show.json.jbuilder
來顯示內容。
1json.id @shipment.id
2json.state @shipment.state
3json.routes @shipment.routes do |route|
4 json.route_id route.id
5 json.delivered_at route.delivered_at.utc
6 json.shipment_id @shipment.id
7end
基本上跟 Command 是差不多的,在這裡我們不一定要追求「消除重複」的處理,因為查詢單個 Shipment 和增加 Route 後的回傳,有可能在未來改變,可以依照當下的狀況處理。
即使遇到要重複使用,也可以考慮採用 Partial 的機制來維護,可以更好的根據使用情境調整。
這一系列的實作,可以看到我們基本上是圍繞在名為 Shipments
的模組進行,大部分的實作都被整合在這個模組之下,讓我們可以更加專注在單一功能上,又不會跟框架、其他模組有耦合的關係,以長期的角度來看整個專案是更加容易維護的。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 軟體架構的挑戰 - 重新思考 Rails 架構
- 資料驅動設計 - 重新思考 Rails 架構
- 複雜的操作 - 重新思考 Rails 架構
- 時區換算 - 重新思考 Rails 架構
- 報表機制 - 重新思考 Rails 架構
- 通用化功能 - 重新思考 Rails 架構
- ActiveRecord 的限制 - 重新思考 Rails 架構
- 領域驅動設計 - 重新思考 Rails 架構
- 從架構到設計 - 重新思考 Rails 架構
- 重復使用的反思 - 重新思考 Rails 架構
- 釐清脈絡 - 重新思考 Rails 架構
- 劃分邊界 - 重新思考 Rails 架構
- 職責劃分 - 重新思考 Rails 架構
- 架構規劃 - 重新思考 Rails 架構
- 驗收測試 - 重新思考 Rails 架構
- Controller - 重新思考 Rails 架構
- Form - 重新思考 Rails 架構
- Use Case - 重新思考 Rails 架構
- Entity - 重新思考 Rails 架構
- Repository - 重新思考 Rails 架構
- Output - 重新思考 Rails 架構
- Query - 重新思考 Rails 架構
- 可能性 - 重新思考 Rails 架構