go 語言運算元據庫 CRUD

leyafo發表於2017-09-07

文章原鏈:http://www.leyafo.com/post/2017-09-07-go-db-crud/

go 語言標準庫已經提供資料庫訪問通用介面,不同資料庫需搭配相應連線 Driver。標準庫裡面 database/sql 實現基本資料型別 Scan, 基本的 Transaction 以及 sql 引數化。用這些東西去運算元據庫完全夠用,只是隨著專案程式碼增長,sql string 會蔓延到專案的各個角落。若要修改資料庫表結構,這些 sql string 就是噩夢一般存在。流行做法是使用 ORM(Object Relational Mapping) 去解決這個問題。ORM 封裝好一些 CRUD 基本操作,可以避免大量手寫 sql string.

go 編譯型語言,無法做到像 Ruby 裡面的 Method Missing 這樣動態特性,可以為一個複合型別的 struct 隨時新增一個 field. go 社群使用的 ORM 還是需要事先手動新增好 Field. 這樣做需要對資料庫和 struct 的成員做很多約定,寫好對應的訪問 tag。這些仍然無法避免修改資料庫表時要一齊修改 go 程式碼。

我們可以仔細想想 ORM 真的是唯一的選擇嗎?ORM 實際上無法完美運算元據庫,它做的事情無非就是這些事情:

  1. 引數化 sql string。
  2. 封裝一些簡單的 CRUD 操作。
  3. 序列化 sql 查詢結果。

對於一些複雜的資料庫查詢語句,任何強大的 ORM 庫,靈活的動態語言特性都無法解決。況且 ORM 也未必是好東西,它簡化了操作,卻間接隱藏了資料庫的一些功能。如果不脫離 ORM 依賴我們無法去更好的運算元據庫,去自己優化查詢語句。以 go 語言目前的特性來看,這個社群永遠也不可能會做出能像動態語言那樣靈活的 ORM。關於 ORM 與 go 語言更詳細的吐槽請見這裡

如果不用 ORM 我們碰到的無非是上面提到的 3 個問題而已。把這個三個問題掰開處理,第一個問題是不存在的。引數化 sql string 這件事情我們只需要小心的處理就能防止 sql 注入這樣的安全問題。剩下的就是 2 和 3 的問題,而第二個問題我們暫時也不需要解決,我們一開始就去寫一些重複程式碼好了。而問題 3 我們可以手動自己去序列化,也可以用社群的一些庫去解決這個問題。社群已經有了不錯的解決方案,我用的就是 sqlx 去解決的這個問題。

當初專案開始時我考察不少 ORM 庫後,果斷拋棄使用 ORM。專案經過一段時間迭代後,不用 ORM 沒有特別明顯的不便。唯一的問題就是我需要手動為每一張表做好 table 到 go struct 之間的對映。需要寫非常多重複性的 CRUD 操作程式碼。這些程式碼很難使用統一方法減少重複,只好任其膨脹。直到有一天我看到這篇文章,原來 go 語言需要寫重複程式碼這個問題在其他問題領域也存在,而社群老司機們解決這個問題的方式是使用機器生成程式碼。社群大範圍這麼做的原因是 go 語言的語法非常簡單,很容易用 go 生成 go 程式碼。

寫 CRUD 生成器思路很簡單,只需要去資料庫裡查詢 table 的詳細資訊,為不同的 column 資料型別對映 go 資料型別。最後用 go 的 template 實現一些簡單的 CRUD 方法。對於一般的 select 查詢語句我這裡沒有去做實現,因為不同的表查詢條件是不一樣的,這種差異用函式傳參解決會更好。生成器做 column 對映和拼接查詢語句是極好的。

最後在專案裡面把生成器生成的程式碼和手寫的資料庫相關程式碼分離開來,以方便資料庫表結構變動後可以讓生成器重新生成程式碼。這裡是我寫的這個簡單的生成器,參照了一部分 xo 專案的程式碼,由於我只支援 Postgresql, 沒有去支援 join 和 has_many 這樣的操作,程式碼相對他們要簡單很多。

事情到這裡並沒有完,我們還需要序列化後的查詢結果做一些反序列化的工作,go 語言的 struct tag 對這方面有很好的支援。可是我們用機器生成的程式碼是無法控制外界需要的各種反序列化的欄位。下面以序列化 json 為例,我們並不想把資料庫的 ID 欄位暴露給外界。如果用生成器去控制這些配置,生成器需要做更多的配置,資料庫表欄位需要更多約定,這無疑會增加生成器的難度。我們必須要為每一張表手動單獨做一個 Export 的結構體與方法,以滿足各種其他序列化的要求。

type UserExport struct {
    Name      string `json:"name"`
    Email     string `json:"email"`
    UpdatedAt string `json:"updated_at"`
}

func (u User) Export() UserExport {
    return UserExport{
        Name:      u.Name,
        Email:     u.Email,
        UpdatedAt: u.UpdatedAt,
    }
}

func (u User) MarshalJSON() ([]byte, error) {
    return json.Marshal(u.Export())
}

如上所示,我們可以為 User 表配置不同 field 和 tag。可以單獨為不同的序列化資料型別寫一個 Marshal 方法,這些都是手動可以控制。為了靈活我們還可以把 Export 這個結構體直接嵌入到其他結構體中,不用擔心 Marshal 會繼承式覆蓋後面 Marshal 方法。

更多原創文章乾貨分享,請關注公眾號
  • go 語言運算元據庫 CRUD
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章