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
}

使用此數據庫的代碼有兩種變體:

示例正在做幾件事:

  1. 將一些數據(帖子、評論、標籤)添加到數據庫;

  2. 查詢帶有指定標籤的所有帖子;

  3. 查詢所有帖子詳細信息(包括附加到其上的所有評論、標記)。

舉個例子,這裏是任務(2)的兩個變體:查找帶有指定標籤的所有帖子(這可能是在博客上填充某種檔案列表頁面)。

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,這種方式相當直接。我們需要在 PostPostTag 之間建立一個內連接, 並使用 tagID 進行條件過濾; 其餘代碼僅僅迭代結果。

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 訪問層的語言中,這種優勢更加強大。

至於缺點:

  1. 要學習另一層,包括所有特性,特殊語法,魔法標籤等。如果您已經熟悉 SQL 本身,那麼這主要是一個缺點;

  2. 即使您沒有 SQL 經驗,也有大量的知識庫和許多可以幫助解答的人。任何一個 ORM 都是更加晦澀的知識,不爲很多人所分享,您將花費大量的時間弄清楚如何使其工作;

  3. 調試查詢性能具有挑戰性,因爲我們從 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