---
title: "Query - 重新思考 Rails 架構"
date: 2024-11-29T00:00:00+08:00
publishDate: 2024-11-29T00:00:00+08:00
lastmod: 2024-06-02T17:03:47+08:00
tags: ["Rails","Domain-Driven Design","設計","Clean Architecture"]
series: "rethink-rails-architecture"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/11/29/rethink-rails-architecture-query/"
language: "zh-tw"
---


前面我們主要著重在寫入的處理，如果是單純寫入的處理，我們可以使用 Query 來處理。基本上 Command 和 Query 都可以看作 Use Case（使用案例）的一種，可以根據複雜程度決定是否需要細分，當我們實踐到這個階段，也接近有讀寫分離的系統所呈現的狀態。

<!--more-->

## Query

Query 的實作跟 Command 基本上是相同的，只是我們更明確的將職責「讀取」和「寫入」區分開來，因此只需要在 `lib/shipments/query` 目錄下增加一個 `find_shipment.rb` 即可。

```ruby
module Shipments
  module Query
    class FindShipment
      attr_reader :shipment_repository

      def initialize(shipment_repository:)
        @shipment_repository = shipment_repository
      end

      def execute(shipment_id)
        shipment_repository.find_shipment(shipment_id.to_i)
      end
    end
  end
end
```

我們在「取得 Shipment」的機制上跟 Command 是同樣的邏輯，這表示我們能用相同的 Repository 來處理，因此就不需要重新處理 Repository 的實作，直接沿用現有的行為。

> Query 也很適合用於處理先前提到的報表情境，因為我們已經將行為限定在報表相關的處理，因此可以集中精神在報表的查詢上，這種情境時，能在 Repository 直接撰寫 SQL 會比 ORM 更加高效，這也是為什麼 Repository 不直接等同於 Rails 的 ActiveRecord。

## 測試{#testing}

撰寫測試是一個好習慣，能確保我們在新增或者修改時不會影響現有的行為，也能更直觀反應每個功能的邏輯。在原本的 `features/shipment.feature` 可以加入新的描述來反應這次新增的功能。

```gherkin
Feature: Shipment
  # ...
  Scenario: I can get the shipment
    Given there have some shipment route
      | route_id | shipment_id | delivered_at         |
      | OKA      | 1           | 2024-03-19T00:00:00Z |
    When I make a GET request to "/shipments/1"
    Then the response should be
      """
      {
        "id": 1,
        "state": "shipping",
        "routes": [
          {
            "route_id": "OKA",
            "shipment_id": 1,
            "delivered_at": "2024-03-19T00:00:00.000Z"
          }
        ]
      }
      """
```

因為有新的步驟，還會需要在 Step Definition 中，針對 `GET` 的情境增加新的定義。

```ruby
When('I make a GET request to {string}') do |path|
  @response = get path
end
```

## 完善功能{#complete-feature}

最後，我們就可以把前面沒有提到的部分加入到 Rails 中。先針對 `config/routes.rb` 調整將預設值設定為 JSON 格式。

```ruby
# ...
  resources :shipments, module: :shipments, defaults: { format: :json } do
    resources :routes, only: [:create], defaults: { format: :json }
  end
```

接著加入 `app/shipments/shipments_controller.rb` 呼叫這次增加的 Query 物件。

```ruby
module Shipments
  class ShipmentsController < ApplicationController
    def show
      query = Shipments::Query::FindShipment.new(shipment_repository: ShipmentRoute)
      @shipment = query.execute(params[:id])
    end
  end
end
```

跟增加 View 的方式相同，這次加入 `app/shipments/shipments/show.json.jbuilder` 來顯示內容。

```ruby
json.id @shipment.id
json.state @shipment.state
json.routes @shipment.routes do |route|
  json.route_id route.id
  json.delivered_at route.delivered_at.utc
  json.shipment_id @shipment.id
end
```

基本上跟 Command 是差不多的，在這裡我們不一定要追求「消除重複」的處理，因為查詢單個 Shipment 和增加 Route 後的回傳，有可能在未來改變，可以依照當下的狀況處理。

即使遇到要重複使用，也可以考慮採用 Partial 的機制來維護，可以更好的根據使用情境調整。

這一系列的實作，可以看到我們基本上是圍繞在名為 `Shipments` 的模組進行，大部分的實作都被整合在這個模組之下，讓我們可以更加專注在單一功能上，又不會跟框架、其他模組有耦合的關係，以長期的角度來看整個專案是更加容易維護的。

