Gorm 源碼解析
我們先通過一張圖來看 Gorm 核心主流程。
gorm 主流程
1. 初始化 DB 連接
使用 database.sql 初始化連接。我們平時所說的數據庫驅動其實就是每個數據庫對 DSN 不同的解析方式,最終底層都是使用的 TCP 建立起數據庫連接。
type Connector interface {
Connect(context.Context) (Conn, error)
Driver() Driver
}
封裝的結構體爲 driver.Connector,通過 connector.Connect 方法來建立 TCP 連接。
以 MySQL 爲例,DSN 裏面使用的爲 TCP。
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
...
// 裏面爲註冊TCP的方法,所以走默認方法註冊
dial, ok := dials[mc.cfg.Net]
if ok {
dctx := ctx
if mc.cfg.Timeout > 0 {
var cancel context.CancelFunc
dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
}
mc.netConn, err = dial(dctx, mc.cfg.Addr)
} else {
nd := net.Dialer{Timeout: mc.cfg.Timeout}
mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
}
...
}
如果發生 err 則直接進行返回,由外層捕捉 error 進行處理。
mysql 使用 open 初始化 DB 連接:
db, err = gorm.Open(mysql.Open(dbDSN), &gorm.Config{}) *// 通過此方法進行連接初始化*
Open 方法流程圖:
可以通過下面的代碼來看到 Open 方法的具體操作
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
// 1.初始化配置,通過opts 來設置可變參數
config := &Config{}
// 2.配置進行應用
if d, ok := dialector.(interface{ Apply(*Config) error }); ok {
if err = d.Apply(config); err != nil {
return
}
}
// 3.初始化gorm.DB對象,後續操作通過clone 該對象進行調用
db = &DB{Config: config, clone: 1}
// 初始化執行函數
db.callbacks = initializeCallbacks(db)
// 4.通過Initialize方法建立連接
if dialector != nil {
config.Dialector = dialector
}
if config.Dialector != nil {
err = config.Dialector.Initialize(db)
}
return}
// 初始化執行函數
func initializeCallbacks(db *DB) *callbacks {
return &callbacks{
processors: map[string]*processor{
"create": {db: db},
"query": {db: db},
"update": {db: db},
"delete": {db: db},
"row": {db: db},
"raw": {db: db},
},
}
}
Initialize 方法通過調用 databases.sql 的 connect 建立起對數據庫的連接。最終建立的連接會通過 driverConn 結構體進行保存
2. 代碼鏈式調用執行語句
以查詢爲作爲例子
var DB *gorm.DB // 此處的DB爲第一步使用Open方法所打開的連接
DB.Where("id = ?", user.ID).First(&newUser)
Where 方法的拼接方式
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
// 此處複製一個DB實例
tx = db.getInstance()
// 將對應的條件進行構造並加入到Statement結構中
// 將類似id = ? 等條件進行轉化
// 構造出 clause.Expression對象
if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return}
func (db *DB) getInstance() *DB {
// 單例模型,Clone 出來後不再進行重複clone
if db.clone > 0 {
tx := &DB{Config: db.Config, Error: db.Error}
// 第一次Clone則直接將Statement的語句進行構造,否則對Statement 進行復制即可
if db.clone == 1 {
// clone with new statement
tx.Statement = &Statement{
DB: tx,
ConnPool: db.Statement.ConnPool,
Context: db.Statement.Context,
Clauses: map[string]clause.Clause{},
Vars: make([]interface{}, 0, 8),
}
} else {
// with clone statement
tx.Statement = db.Statement.clone()
tx.Statement.DB = tx
}
return tx
}
return db
}
clause.Expression 爲 Interface ,SQL 各種表達通過實現 Build 方法來生成對應字符串。以下爲對應的部分 UML 圖:
Expression UML 圖
3. 真正執行
執行方法流程圖
callbacks 包下面包含全部 gorm 自帶的方法,以查詢方法爲例進行講解構建 SQL:
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) { queryCallback := db.Callback().Query()
*// 註冊相應的查詢方法*
queryCallback := db.Callback().Query()
queryCallback.Register("gorm:query", Query)
queryCallback.Register("gorm:preload", Preload)
queryCallback.Register("gorm:after_query", AfterQuery)
queryCallback.Clauses = config.QueryClauses
}
func Query(db *gorm.DB) {
if db.Error == nil {
// 1.構建查詢的SQL
BuildQuerySQL(db)
// 2.真正對語句進行執行,並返回對應的Rows結果
if !db.DryRun && db.Error == nil {
rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
gorm.Scan(rows, db, 0)
}
}
}
func BuildQuerySQL(db *gorm.DB) {
// 核心構建語句,通過Build 拼接出對應的字符串
db.Statement.Build(db.Statement.BuildClauses...)
}
- 項目接入
至此我們從源碼的級別瞭解了 Gorm 的執行流程,但是如果要用到我們實際的開發中,我們直接引用 Gorm 就好嗎?
架構設計中有一個點:不要強依賴框架。
框架的實現更多的是細節,屬於固件代碼,當一個框架被淘汰之後,如果我們的代碼要修改依賴,如果是直接依賴的話修改起來會比較麻煩,每個使用的業務方都需要進行修改。
我們直接使用了 gorm ,在項目啓動的時候確實會相對來說方便一些,但是試想如果有一天公司有一個團隊做了自己的 ORM 框架並且有了相應的性能優化,希望推廣開來給各個團隊都使用。
這個時候我們要替換的話就需要在每一個地方的 gorm 都進行修改,並且還會存在一個問題就是修改的過程中可能會出錯和不兼容的問題。
在項目啓動初期可以看公司是否有公共工具包的封裝倉庫。
如果有,並且已經提供了 ORM 的功能,我們可以直接進行使用。
如果沒有,那麼我們可以在裏面新增 ORM 工具包的封裝。
我們有兩種實現方式:
-
直接設計一個數據庫訪問的 RPC client
-
將
gorm
和xorm
等通過 代理的形式,在切面上增加自己需要的功能。
第一種封裝方式需要的人力較大,但是對公司來說,能比較靈活的去接入中間件,定製化能力強,比較適合規模較大的公司去用。
第二種方式適合大部分中小型公司的情況,不需要完全自研,但是又可以在原來的包上二次封裝提供 SDK 不存在的功能。在前面再增加一層 interface 後,依賴抽象後可以靈活替換底層具體實現,這樣也可以將底層不同的 SDK 統一進行封裝,這樣可以加上自定義日誌和統計信息上報等。
下圖最左邊自己設計一個 RPC client,中間的是直接使用 gorm,最右邊是通過代理開源包的模式。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Vt179x1HbdTXtCbQlbZ1vg