ORM 還是 非 ORM?
我一直很喜歡使用 Go 的 database/sql 包來處理數據庫。最近,一些涉及 Gorm 的問題激起了我對 Go 中 使用 ORM
vs. 直接使用 database/sql
的好奇心。在 ORM 方面曾有過豐富的經驗,所以我決定開始一個實驗:利用 Gorm 和 非 ORM 編寫同一個簡單的應用程序,並比較付諸的努力。
這促使我寫下了一些關於 ORM 優缺點的想法。如果您對此感興趣,請繼續閱讀!
非 ORM vs. ORM 的相關經驗
實驗中,定義了一個可作爲博客引擎子集的簡單數據庫,同時編寫一些操作和查詢該數據庫的 Go 代碼,並比較使用純 SQL 與使用 ORM 的表現。
數據庫表如下:
儘管很簡單,這些表展示了一個慣用的、規範的數據庫,基本包含構建簡單 wiki 或博客應用程序所需的所有元素:它同時具有一對多的關係(帖子與評論)和多對多關係(帖子與標籤)。如果您更喜歡數據庫 SQL 語句,這是代碼示例 [1] 中的定義:
create table Post (
postID integer primary key,
published date,
title text,
content text
);
create table Comment (
commentID integer primary key,
postID integer,
author text,
published date,
content text,
-- One-to-many relationship between Post and Comment; each Comment
-- references a Post it's logically attached to.
foreign key(postID) references Post(postID)
);
create table Tag (
tagID integer primary key,
name text unique
);
-- Linking table for the many-to-many relationship between Tag and Post
create table PostTag (
postID integer,
tagID integer,
foreign key(postID) references Post(postID),
foreign key(tagID) references Tag(tagID)
);
這個 SQL 用 SQLite 測試過;其他 RDBMS 可能需要進行微調。使用 Gorm 時,沒必要寫此 SQL;作爲替代,我們定義 “對象”(實際上是結構體), 並附帶上 Gorm 的一些魔法 tag:
type Post struct {
gorm.Model
Published time.Time
Title string
Content string
Comments []Comment `gorm:"foreignkey:PostID"`
Tags []*Tag `gorm:"many2many:post_tags;"`
}
type Tag struct {
gorm.Model
Name string
Posts []*Post `gorm:"many2many:post_tags;"`
}
type Comment struct {
gorm.Model
Author string
Published time.Time
Content string
PostID int64
}
使用此數據庫的代碼有兩種變體:
-
no-ORM:通過 database/sql 包使用純 SQL 查詢;
-
ORM:使用 Gorm 庫進行數據庫訪問。
示例正在做幾件事:
-
將一些數據(帖子、評論、標籤)添加到數據庫;
-
查詢帶有指定標籤的所有帖子;
-
查詢所有帖子詳細信息(包括附加到其上的所有評論、標記)。
舉個例子,這裏是任務(2)的兩個變體:查找帶有指定標籤的所有帖子(這可能是在博客上填充某種檔案列表頁面)。
- 首先,no-ORM:
func dbAllPostsInTag(db *sql.DB, tagID int64) ([]post, error) {
rows, err := db.Query(`
select Post.postID, Post.published, Post.title, Post.content
from Post
inner join PostTag on Post.postID = PostTag.postID
where PostTag.tagID = ?`, tagID)
if err != nil {
return nil, err
}
var posts []post
for rows.Next() {
var p post
err = rows.Scan(&p.Id, &p.Published, &p.Title, &p.Content)
if err != nil {
return nil, err
}
posts = append(posts, p)
}
return posts, nil
}
如果您瞭解 SQL,這種方式相當直接。我們需要在 Post
和 PostTag
之間建立一個內連接, 並使用 tagID
進行條件過濾; 其餘代碼僅僅迭代結果。
- 接下來,ORM:
func allPostsInTag(db *gorm.DB, t *Tag) ([]Post, error) {
var posts []Post
r := db.Model(t).Related(&posts, "Posts")
if r.Error != nil {
return nil, r.Error
}
return posts, nil
}
在 ORM 代碼中,爲獲得相同的效果, 我們傾向於直接使用對象(此處爲 Tag
)而非 ID。由 Gorm 生成的 SQL 查詢與我在 no-ORM 變體中手動編寫的 SQL 查詢幾乎相同。
除了爲我們生成 SQL 之外,Gorm 還提供了一種更簡單的方法來填充結果。在使用 database/sql 的代碼中,我們顯式地迭代結果,將每一行分別掃描到單獨的結構體字段中。Gorm 的相關方法(以及其他類似的查詢方法)將自動填充結構體,並且還將一次掃描整個結果集。
隨意玩代碼!令我驚喜的是 Gorm 在此節約代碼量(對於 DB 部分的代碼,節省約 50%),並且對於這些簡單的查詢,使用 Gorm 並不難:直接從 API 文檔中獲取調用方式。我對具體示例的唯一抱怨是,在 Post 和 Tag 之間設置多對多關係有點困難,Gorm 字段的 tag 看起來也很醜陋和魔幻。
分層的複雜性讓人頭疼
像上面那樣的簡單實驗的問題在於,通常很難勾勒出系統的邊界。它顯然適用於簡單的情況,但我有興趣瞭解當它被推到極限時會發生什麼:它如何處理複雜的查詢和數據庫模式 (schema)?因此我開始瀏覽 Stack Overflow,那兒有許多與 Gorm 相關的問題,當然足以確信,通常的分層複雜性問題是顯而易見的(例 1, 例 2)。讓我解釋一下我的意思。
當包裝層本身很複雜時,任何將複雜功能包含在其中的情況,都有增加整體複雜性的風險。這通常伴隨着 leaky abstractions
: 包裹層無法完成包裝底層功能的完美工作,將迫使程序員同時與兩個層進行鬥爭。
不幸的是,Gorm 非常容易受到這個問題的影響。Stack Overflow 提供了無窮無盡的問題,用戶最終需應對由 Gorm 本身強加的複雜性,解決其侷限性等問題。很少有事情如此讓人惱火:確切地知道您想要什麼(例如,您想要發出哪個 SQL 查詢),但是卻無法編寫出 Gorm 查詢時最終調用的正確代碼。
使用 ORM 的利弊
從我的實驗中可以明顯看出使用 ORM 的一個關鍵優勢:它可以節省相當多的繁瑣編碼。以 DB 爲中心的代碼節省約 50% 是非常重要的,這可以爲某些應用程序帶來真正的改變;另一個不明顯的優點是從不同的數據庫後端抽象。然而,這在 Go 中可能不是一個問題,因爲 database/sql 已經提供了一個很好的可移植層。在缺乏標準化 SQL 訪問層的語言中,這種優勢更加強大。
至於缺點:
-
要學習另一層,包括所有特性,特殊語法,魔法標籤等。如果您已經熟悉 SQL 本身,那麼這主要是一個缺點;
-
即使您沒有 SQL 經驗,也有大量的知識庫和許多可以幫助解答的人。任何一個 ORM 都是更加晦澀的知識,不爲很多人所分享,您將花費大量的時間弄清楚如何使其工作;
-
調試查詢性能具有挑戰性,因爲我們從
metal
進一步抽象了一個級別。有時需要進行相當多的調整才能讓 ORM 爲您生成正確的查詢,當您已經知道需要哪些查詢時,這很令人沮喪。
最後,一個缺點只會在長期內變得明顯:雖然 SQL 多年來保持相當穩定,但 ORM 是特定於語言的,並且往往會出現和消失。每種流行語言都有各種各樣的 ORM 可供選擇; 當您從一個團隊 / 公司 / 項目轉移到另一個團隊 / 公司 / 項目時,您可能需要轉換,這是額外的精神負擔。或者您可能完全切換語言。SQL 是一個更加穩定的層,可以跨團隊 / 語言 / 項目與您保持聯繫。
結論
使用原生 SQL 實現了一個簡單的應用程序框架,並將其與使用 Gorm 的實現進行了比較後,我可以看到 ORM 在減少格式化代碼方面的吸引力。我也記得多年前自己是一個 DB 新手時,使用 Django 及其 ORM 來實現一個應用程序:它很好!我沒有必要過多考慮 SQL 或底層數據庫,它就可以。但那個用例確實非常簡單。
隨着我經驗越來越豐富,我也看到使用 ORM 的許多缺點。尤其,我不認爲在 Go 這種語言中 ORM 對我有用,因爲 Go 已經擁有一個很好的 SQL 接口,幾乎可以跨數據庫後端移植。我寧願花多一點時間敲代碼,但這樣可以節省我閱讀 ORM 文檔、優化查詢、尤其是調試的時間。
如果您的工作是編寫大量簡單的類似 CRUD 的應用程序,那麼我可以看到 ORM 在 Go 中仍然有用,其節省的代碼量克服了這些缺點。最後,所有這些都歸結爲這一中心論點即, Benefits of dependencies in software projects as a function of effort[2]:在我看來,在一個並不屬於簡單的 CRUD 應用程序上,於 DB 接口相關代碼之外花費大量精力,ORM 依賴並不值得。
via: https://eli.thegreenplace.net/2019/to-orm-or-not-to-orm/
作者:Eli Bendersky[3] 譯者:zhoudingding[4] 校對:polaris1119[5]
本文由 GCTT[6] 原創編譯,Go 中文網 [7] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
代碼示例: https://github.com/eliben/code-for-blog/tree/master/2019/orm-vs-no-orm/sql
[2]
Benefits of dependencies in software projects as a function of effort: https://eli.thegreenplace.net/2017/benefits-of-dependencies-in-software-projects-as-a-function-of-effort/
[3]
Eli Bendersky: https://eli.thegreenplace.net/
[4]
zhoudingding: https://github.com/dingdingzhou
[5]
polaris1119: https://github.com/polaris1119
[6]
GCTT: https://github.com/studygolang/GCTT
[7]
Go 中文網: https://studygolang.com/
福利
我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IJ4oxnG0HekI_5jXHqzEhg