【編者按】ORM(對象關係對映)在軟體開發中受到爭議,常見批評包括違反 SOLID 原則和效率低,但實際問題在於透明性和調試困難。如果能夠正確使用 ORM ,那麼它的執行效率也會很高,但很多開發者往往過度依賴主機語言而沒有充分利用 ORM 的原生 SQL 功能。
原文連結:https://github.com/getlago/lago/wiki/Is-ORM-still-an-%27anti-pattern%27%3F
未經允許,禁止轉載!
作者 | lago 譯者 | 明明如月
責編 | 夏萌

前言
對象關係對映(ORM)常常是軟體開發者熱議的焦點。
你可以在網路上發現大量文章唱反調:「ORM 是反模式,它們只是創業公司的小玩具,整體來說弊大於利。」 這其實有些過分誇大其詞。ORM 並非全無用處。正如軟體領域裡的技術一樣,完美的 ORM 並不存在。然而,對 ORM 的批評也在意料之中——若是兩年前,我可能也會全盤接受這些刻板印象。我也曾有過類似 “你是說 ORM 把伺服器的記憶體耗盡了?” 的遭遇。
但是,實際情況是 ORM 框架被誤用的例子要比被濫用的例子還要多。
這篇試圖辯護 ‘ORM 其實並沒有那麼糟糕’ 的文章,源自我們在 Lago 公司因為 ORM 遭遇的一次不愉快經歷。這次經歷使我們開始對我們對 Ruby on Rails ORM,即 Active Record 的依賴產生疑問。如果要為這篇文章取一個更具吸引力的標題的話,可能會是 「ORM 的確有些問題」。但在深入思考這個問題後,我們認為 ORM 框架並不糟。它們只是一種抽象工具,有其優點和缺點——它們抽象化了一些可見性,偶爾會導致一些性能損失。僅此而已。
今天,我們就來深入探討 ORM,分析常見的批評和聊聊真正的問題所在。

兩種正規化的對比
我們從一個簡單的問題開始:ORM 和資料庫遵循兩種不同的正規化。
ORM 創建對象(這就是 O 的來源)。對象就像有向圖——節點指向其他節點,但並不一定互相指向。而相反,資料庫的關係表包含的資料總是通過共享鍵(也就是無向圖)進行雙向連結。
從技術角度來看,ORM 可以通過強制實現雙向指針來模擬無向圖。但實際上,這樣做並不容易;很多開發者最後可能得到的是缺少 Posts 陣列的 User 對象 或者 Posts 陣列實體缺少對同一 User 對象的反向引用(但可能是一個克隆對象)。

ORM 和關係資料庫遵循兩種不同的正規化。例如,ORM 可能會將使用者(User)的帖子(Post)以陣列的形式返回,但不會在每個帖子(Post)中包含對 User(作者)的反向引用。
然而,這種正規化的不匹配並非無法解決。ORM 和關係資料庫最終都只是圖形呈現;只是資料庫只有非定向邊。儘管這對 ORM 來說是一個學術上較有道理的批評,但 ORM 的真正問題其實更為「深刻」。

打破的原則
在深入討論”細節問題”之前,我們首先討論一些更基礎的原則。關於 ORM 框架的常見抱怨之一就是它們違反了在軟體設計課程中所教授的 SOLID 原則中的兩條。如果你不了解 SOLID,它是一個首字母縮略詞,代表了一些重要的設計原則。
單一職責原則(SRP)
ORM 被批評違反了單一職責原則(SRP)。SRP 要求一個類應該只有一個存在的目的。然而,ORM 並未達到這一標準。在較高的抽象層面上,它們處理了”所有的資料庫事務”,但這就好比創建一個處理”所有應用事務”的單一類。JohnoTheCoder 對此有一個非常好的解釋:ORM (i) 創建了用於和資料庫互動的類,(ii) 代表了一個記錄,以及 (iii) 定義了關係。我還會進一步補充說,ORM (iv) 創建並執行了遷移。
除了你,還有其他人對這種針對 ORM 的語義上的批評感到不滿。我也認為這種常見的反對 ORM 的論點有點模糊。畢竟,ORM 的主要工作就是彌補兩種根本不同的資料正規化之間的差距;它當然會打破一些原則。
關注點分離(SOC)
關注點分離(SOC)的思想和 SRP 相似,但是應用在不同的層面。SOC 規定,強調將不同的功能分離開來,使得每個模組可以獨立地開發、測試和維護。然而,ORM 將資料庫管理從後端轉移到了資料庫本身,違反了 SOC。但在如今 SOC 這種原則已經有些過時了。現在,基礎設施元件和編碼模式正在協同合作,以實現更好的性能(比如在 OLAP 資料庫中的 CPU 聚合器)、更低的延遲(例如在前後端中間的邊緣運算)以及更清晰的程式碼(例如使用 Monorepo 倉庫程式碼管理模式)。

真正的問題
現在我們已經詳細地討論了這些”假”問題,讓我們來看看 ORM 的真正面臨的問題。ORM 框架採取較為守舊的方法。它們使用一種可預測和可重複的查詢系統,這種系統本身並沒有被最佳化或視覺化。然而,ORM 框架的開發者們意識到了這一點;他們已經添加了許多功能來解決這些問題,自 Active Record 首次亮相以來,他們已經取得了巨大的進步。
效率
開發者普遍認為 ORM 框架性能不佳。
這種說法在很大程度上是不準確的。實際上,ORM 框架的效率通常要比許多開發者想象的要高。然而,由於依賴主機語言(例如 JavaScript 或 Ruby)來組合資料過於簡單,ORM 的使用確實引導了一些不良的開發實踐。
例如,看一看這段使用 JavaScript 擴展資料條目的效率低下的 TypeORM 程式碼:
const authorRepository = connection.getRepository(Author);
const postRepository = connection.getRepository(Post);
// Fetch all authors who belong to a certain company
const authors = await authorRepository.find({ where: { company: 'Hooli' } });
// Loop through each author and update their posts separately
for (let i = 0; i < authors.length; i++) {
const posts = await postRepository.find({ where: { author: authors[i] } });
// Update each post separately
for (let j = 0; j < posts.length; j++) {
posts[j].status = 'archived';
await postRepository.save(posts[j]);
}
}
相反,開發者應該使用 TypeORM 的內建功能構造單個查詢:
const postRepository = connection.getRepository(Post);
await postRepository
.createQueryBuilder()
.update(Post)
.set({ status: 'archived' })
.where("authorId IN (SELECT id FROM author WHERE company = :company)", { company: 'Hooli' })
.execute();
在 Lago 的對賬單 SQL 重構中有一個很好的例子。Active Record 的問題與其可見性有關(下面會詳細討論)。我們的 ORM 和原生 SQL 查詢的性能相當。由於我們大量使用了 Active Record 的資料聯接功能,我們的查詢已經過最佳化:
InvoiceSubscription
.joins('INNER JOIN subscriptions AS sub ON invoice_subscriptions.subscription_id = sub.id')
.joins('INNER JOIN customers AS cus ON sub.customer_id = cus.id')
.joins('INNER JOIN organizations AS org ON cus.organization_id = org.id')
.where("invoice_subscriptions.properties->>'timestamp' IS NOT NULL")
.where(
"DATE(#{Arel.sql(timestamp_condition)}) = DATE(#{today_shift_sql(customer: 'cus', organization: 'org')})",
today,
)
.recurring
.group(:subscription_id)
.select('invoice_subscriptions.subscription_id, COUNT(invoice_subscriptions.id) AS invoiced_count')
.to_sql
它被下面這個重寫的原生 SQL 所替代: