ActiveRecord 的限制 - 重新思考 Rails 架構
當我們要透過 Ruby on Rails 這個框架來開發這樣的系統時,該如何進行設計呢?大多數時候我們會以模型(Model)為基礎思考,這在 MVC(Model ViewController)框架上還算常見,因為核心邏輯大多會被實作在 Model 上。
ActiveRecord
在 Rails 中最常見的就是 ActiveRecord 這種架構模式,所有的 Model 都會繼承自 ActiveRecord::Base
這個類型(Class)來實作。同時 ActiveRecord 能夠自動的將資料表欄位進行對應,因此我們可以很簡單的實現跟資料庫的互動。
這也讓我們會很習慣以「資料」的角度去思考 Model 該如何進行設計,以 PChome 下單後「分庫出貨」的例子,我們可能會像這樣設計。
分庫出貨是指商品從不同倉庫送出,因此在 PChome 的訂單畫面上會有兩筆運送紀錄。
1class Order < ApplicationRecord
2 validates :shipped_at, presence: true, if: :shipped?
3 validate :allow_mark_shipped, if: :shipped?
4
5 enum state: {
6 # ...
7 shipped: 'shipped'
8 }
9
10 # ...
11
12 private
13
14 def allow_mark_shipped
15 return if shippings.all?(&:shipped?)
16
17 errors.add(:state, :all_products_not_shipped)
18 end
19end
我們會用一個狀態(State)欄位來保存訂單的狀態,並且還會有一個送達時間(Shipped At)來記錄送達的時間,同時還需要在修改成特定狀態時,要再加以驗證是否滿足改變狀態的條件。
然而,如果要表示「送達」的狀態,所有的運送狀態(shippings
)都是送達時,我們是否還需要有一個「送達狀態」去表示呢?
同樣的,訂單的送達時間應該會是運送狀態中最晚送達的那一筆紀錄。
思考誤區
習慣了 ActiveRecord 的特性後,我們很容易的對於物件狀態的思考有一些盲點,會認為必須存在「資料欄位」才能夠去反應物件(或模型)的狀態。
實際上,我們在訂單上的實作並不需要運送狀態以其送達時間的欄位,完全能透過運送資訊來滿足這些資訊呈現。
1class Order < ApplicationRecord
2 # ...
3
4 def shipped?
5 shippings.all?(&:shipped?)
6 end
7
8 def shipped_at
9 shippings.max_by(&:shipped_at)&.shippted_at
10 end
11end
這個時候,我們可能會開始考慮一些問題。
例如,我們每次都用 shippings
篩選資料,是否會讓運行速度變慢之類的?然而 Rails 會幫我們快取(Cache)關聯的查詢,一筆訂單的運送狀態也不會太多,運算上的消耗幾乎是可以忽略不計的。
另一方面,可能會以這樣進行查詢時,以 JOIN Query 去篩選狀態、時間,會讓查詢變得緩慢。這點的確是正確的,不過我們需要考量一些問題。
我們的資料庫在多少資料量會變得「緩慢」需要評估,如果真的變慢需要在 orders
資料表加上 shipped_at
欄位時,這個欄位的角色是什麼?
以加速查詢來看,shipped_at
這個欄位跟 Rails 提供的 Counter Cache 機制相同,是一種快取機制,實際上這個欄位可能不該由 Order
自己進行管理,而是 Shipping
判斷所有相關的商品都送達後,通知 Order
進行更新(Counter Cache 也是由被統計的物件觸發)
如果仔細思考,當系統需要做的處理、判斷變更多、更複雜,會有非常多細節很難用資料表直接映射(Mapping)成物件來看待。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 軟體架構的挑戰 - 重新思考 Rails 架構
- 資料驅動設計 - 重新思考 Rails 架構
- 複雜的操作 - 重新思考 Rails 架構
- 時區換算 - 重新思考 Rails 架構
- 報表機制 - 重新思考 Rails 架構
- 通用化功能 - 重新思考 Rails 架構
- ActiveRecord 的限制 - 重新思考 Rails 架構
- 領域驅動設計 - 重新思考 Rails 架構
- 從架構到設計 - 重新思考 Rails 架構
- 重復使用的反思 - 重新思考 Rails 架構
- 釐清脈絡 - 重新思考 Rails 架構
- 劃分邊界 - 重新思考 Rails 架構
- 職責劃分 - 重新思考 Rails 架構
- 架構規劃 - 重新思考 Rails 架構