本篇將和大家探討 go 語(yǔ)言中最流行的 orm 框架 ——gorm 的底層實(shí)現(xiàn)原理.
本篇分享內(nèi)容的目錄大綱如下所示:
圖片
圖片
gorm 框架是國(guó)內(nèi)的大神 jinzhu 基于 go 語(yǔ)言開(kāi)源實(shí)現(xiàn)的一款數(shù)據(jù)庫(kù) orm 框架. 【gorm】一詞恢弘大氣,前綴 go 代表 go 語(yǔ)言, 后綴 orm 全稱(chēng) Object Relation Mapping,指的是使用對(duì)象映射的方式,讓使用方能夠像操作本地對(duì)象實(shí)例一樣輕松便捷地完成遠(yuǎn)端數(shù)據(jù)庫(kù)的操作.
gorm 框架開(kāi)源地址為: https://github.com/go-gorm/gorm
本期會(huì)涉及到大量 gorm 的源碼走讀環(huán)節(jié),使用的代碼版本為 tag: v.1.25.5
gorm 框架通過(guò)一個(gè) gorm.DB 實(shí)例來(lái)指代我們所操作的數(shù)據(jù)庫(kù). 使用 gorm 的第一步就是要通過(guò) Open 方法創(chuàng)建出一個(gè) gorm.DB 實(shí)例,其中首個(gè)入?yún)檫B接器 dialector,本身是個(gè)抽象的 interface,其實(shí)現(xiàn)類(lèi)關(guān)聯(lián)了具體數(shù)據(jù)庫(kù)類(lèi)型.
本文將統(tǒng)一以 mysql 為例,注入 gorm.io/driver/mysql 包下定義的 dialector 類(lèi).
package mysqlimport ( "gorm.io/driver/mysql" "gorm.io/gorm")var ( // 全局 db 模式 db *gorm.DB // 單例工具 dbOnce sync.Once // 連接 mysql 的 dsn dsn = "username:password@(ip:port)/database?timeout=5000ms&readTimeout=5000ms&writeTimeout=5000ms&charset=utf8mb4&parseTime=true&loc=Local")func getDB()(*gorm.DB, error){ var err error dbOnce.Do(func(){ // 創(chuàng)建 db 實(shí)例 db, err = gorm.Open(mysql.Open(dsn),&gorm.Config{}) }) return db,err}本文將以 gorm.Open 方法為入口,在第 3 章中深入源碼底層鏈路.
基于 orm 的思路,與某張數(shù)據(jù)表所關(guān)聯(lián)映射的是 po (persist object)模型.
定義 po 類(lèi)時(shí),可以通過(guò)聲明 TableName 方法,來(lái)指定該類(lèi)對(duì)應(yīng)的表名.
type Reward struct { gorm.Model Amount sql.NullInt64 `gorm:"column:amount"` Type string `gorm:"not null"` UserID int64 `gorm:"not null"`}func (r Reward) TableName() string { return "reward"}
定義 po 類(lèi)時(shí),可以通過(guò)組合 gorm.Model 的方式,完成主鍵、增刪改時(shí)間等4列信息的一鍵添加,并且由于聲明了 DeletedAt 字段,gorm 將會(huì)默認(rèn)會(huì)啟動(dòng)軟刪除模式. (有關(guān)軟刪除的內(nèi)容,可參見(jiàn)前文——gorm 框架使用教程)
type Model struct { ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt DeletedAt `gorm:"index"`}下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)查詢(xún)操作的代碼示例. 本文第 4 章會(huì)以 db.First(...) 方法為入口,展開(kāi)底層源碼鏈路的走讀.
func Test_query(t *testing.T) { // 獲取 db db, _ := getDB() // 超時(shí)控制 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 查詢(xún) var r Reward if err := db.WithContext(ctx).First(&r).Error; err != nil { t.Error(err) return } t.Logf("reward: %+v", r)}下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)創(chuàng)建操作的代碼示例. 本文第 5 章會(huì)以 db.Create(...) 方法為入口,展開(kāi)底層源碼鏈路的走讀.
func Test_create(t *testing.T) { // 獲取 db 實(shí)例 db, _ := getDB() // 構(gòu)造 po 實(shí)例 r := Reward{ Amount: sql.NullInt64{ Int64: 0, Valid: true, }, Type: "money", UserID: 123, } // 超時(shí)控制 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 創(chuàng)建 if err := db.WithContext(ctx).Create(&r).Error; err != nil { t.Error(err) return }}下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)刪除操作的代碼示例. 本文第 6 章會(huì)以 db.Delete(...) 方法為入口,展開(kāi)底層源碼鏈路的走讀.
func Test_delete(t *testing.T) { // 獲取 db 實(shí)例 db, _ := getDB() // 構(gòu)造 po 實(shí)例 r := Reward{ } // 超時(shí)控制 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 更新主鍵 id 為 1 的記錄 if err := db.WithContext(ctx).Delete(&r,1).Error; err != nil { t.Error(err) return }}下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)更新操作的代碼示例. 本文第 7 章會(huì)以 db.Update(...) 方法為入口,展開(kāi)底層源碼鏈路的走讀.
func Test_update(t *testing.T) { // 獲取 db 實(shí)例 db, _ := getDB() // 構(gòu)造 po 實(shí)例 r := Reward{ Amount: sql.NullInt64{ Int64: 1000, Valid: true, } } // 超時(shí)控制 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 更新主鍵 id 為 2 的記錄,將金額設(shè)置為 1000 if err := db.WithContext(ctx).Where("id = ?",2).Update(&r).Error; err != nil { t.Error(err) return }}下面展示的是使用 gorm 開(kāi)啟事務(wù)的代碼示例. 本文第 8 章會(huì)以 db.Transaction(...) 方法為入口,展開(kāi)底層源碼鏈路的走讀.
func Test_tx(t *testing.T) { // 獲取 db 實(shí)例 db, _ := getDB() // 事務(wù)內(nèi)的執(zhí)行邏輯 do := func(ctx context.Context, tx *gorm.DB) error { // do somethine ... return nil } // 超時(shí)控制 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 執(zhí)行事務(wù) if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // do ... err := do(ctx, tx) // do ... return err }); err != nil { t.Error(err) return }}
本章中,我會(huì)首先向大家介紹 gorm 框架中各個(gè)核心類(lèi)的定義.
gorm.DB 是 gorm 定義的數(shù)據(jù)庫(kù)類(lèi). 所有執(zhí)行的數(shù)據(jù)庫(kù)的操作都將緊密?chē)@這個(gè)類(lèi),以鏈?zhǔn)秸{(diào)用的方式展開(kāi). 每當(dāng)執(zhí)行過(guò)鏈?zhǔn)秸{(diào)用后,新生成的 DB 對(duì)象中就存儲(chǔ)了一些當(dāng)前請(qǐng)求特有的狀態(tài)信息,我們把這種對(duì)象稱(chēng)作“會(huì)話”.
DB 類(lèi)中的核心字段包括:
? Config:用戶(hù)自定義的配置項(xiàng)
? Error:一次會(huì)話執(zhí)行過(guò)程中遇到的錯(cuò)誤
? RowsAffected:該請(qǐng)求影響的行數(shù)
? Statement:一次會(huì)話的狀態(tài)信息,比如請(qǐng)求和響應(yīng)信息
? clone:會(huì)話被克隆的次數(shù). 倘若 clone = 1,代表是始祖 DB 實(shí)例;倘若 clone > 1,代表是從始祖 DB 克隆出來(lái)的會(huì)話
DB 類(lèi)定義的代碼如下:
// gorm 中定義的數(shù)據(jù)庫(kù)類(lèi)// 所有 orm 的思想type DB struct { // 配置 *Config // 錯(cuò)誤 Error error // 影響的行數(shù) RowsAffected int64 // 會(huì)話狀態(tài)信息 Statement *Statement // 克隆次數(shù) clone int}DB 類(lèi)的 AddError 方法,用于在會(huì)話執(zhí)行過(guò)程中拋出錯(cuò)誤.
一次會(huì)話在執(zhí)行過(guò)程中可能會(huì)遇到多個(gè)錯(cuò)誤,因此會(huì)通過(guò) error wrapping 的方式,實(shí)現(xiàn)錯(cuò)誤的拼接.
func (db *DB) AddError(err error) error { if err != nil { // ... if db.Error == nil { db.Error = err } else { db.Error = fmt.Errorf("%v; %w", db.Error, err) } } return db.Error}
圖片
請(qǐng)求在執(zhí)行時(shí),需要明確操作的是哪張數(shù)據(jù)表.
使用方可以通過(guò)鏈?zhǔn)秸{(diào)用 DB.Table 方法,顯式聲明本次操作所針對(duì)的數(shù)據(jù)表,這種方式的優(yōu)先級(jí)是最高的.
func (db *DB) Table(name string, args ...interface{}) (tx *DB) { tx = db.getInstance() // ... tx.Statement.Table = name // ... return}在 DB.Table 方法缺省的情況下,gorm 則會(huì)嘗試通過(guò) po 類(lèi)的 TableName 方法獲取表名.
在 gorm 中聲明了一個(gè) tabler interface:
type Tabler interface { TableName() string}倘若 po 模型聲明了 TableName 方法,則隱式實(shí)現(xiàn)了該 interface,在處理過(guò)程中會(huì)被斷言成 tabler 類(lèi)型,然后調(diào)用 TableName 方法獲取其表名.
該流程對(duì)應(yīng)的源碼展示如下:
func (stmt *Statement) ParseWithSpecialTableName(value interface{}, specialTableName string) (err error) { // ... stmt.Schema, err = schema.ParseWithSpecialTableName(value, stmt.DB.cacheStore, stmt.DB.NamingStrategy, specialTableName) // ... stmt.Table = stmt.Schema.Table // ... return err}// ParseWithSpecialTableName get data type from dialector with extra schema tablefunc ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Namer, specialTableName string) (*Schema, error) { if dest == nil { return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, dest) } // ... modelType := reflect.Indirect(value).Type() // ... modelValue := reflect.New(modelType) tableName := namer.TableName(modelType.Name()) // 將 po 模型斷言成 tabler interface,然后調(diào)用 TableName 方法獲取表名 if tabler, ok := modelValue.Interface().(Tabler); ok { tableName = tabler.TableName() } // ... // 將表名信息添加到 schema 當(dāng)中 schema := &Schema{ // ... Table: tableName, // ... } // ... return schema, schema.err}接下來(lái)介紹的是 gorm 中非常核心的 statement 類(lèi),里面存儲(chǔ)了一次會(huì)話中包含的狀態(tài)信息,比如請(qǐng)求中的條件、sql 語(yǔ)句拼接格式、響應(yīng)參數(shù)類(lèi)型、數(shù)據(jù)表的名稱(chēng)等等.
statement 類(lèi)中涉及到的各個(gè)核心字段通過(guò)下方的代碼和注釋加以介紹:
// Statement statementtype Statement struct { // 數(shù)據(jù)庫(kù)實(shí)例 *DB // ... // 表名 Table string // 操作的 po 模型 Model interface{} // ... // 處理結(jié)果反序列化到此處 Dest interface{} // ... // 各種條件語(yǔ)句 Clauses map[string]clause.Clause // ... // 是否啟用 distinct 模式 Distinct bool // select 語(yǔ)句 Selects []string // selected columns // omit 語(yǔ)句 Omits []string // omit columns // join Joins []join // ... // 連接池,通常情況下是 database/sql 庫(kù)下的 *DB 類(lèi)型. 在 prepare 模式為 gorm.PreparedStmtDB ConnPool ConnPool // 操作表的概要信息 Schema *schema.Schema // 上下文,請(qǐng)求生命周期控制管理 Context context.Context // 在未查找到數(shù)據(jù)記錄時(shí),是否拋出 recordNotFound 錯(cuò)誤 RaiseErrorOnNotFound bool // ... // 執(zhí)行的 sql,調(diào)用 state.Build 方法后,會(huì)將 sql 各部分文本依次追加到其中. 具體可見(jiàn) 2.5 小節(jié) SQL strings.Builder // 存儲(chǔ)的變量 Vars []interface{} // ...}這里額外強(qiáng)調(diào)一下 connPool 字段,其含義是連接池,和數(shù)據(jù)庫(kù)的交互操作都需要依賴(lài)它才得以執(zhí)行. connPool 本身是個(gè) interface,定義如下:
type ConnPool interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row}connPool 根據(jù)是否啟用了 prepare 預(yù)處理模式,存在不同的實(shí)現(xiàn)類(lèi)版本:
此處額外介紹一下 DB 的克隆流程,所有在始祖 DB 基礎(chǔ)上追加狀態(tài)信息,克隆出來(lái)的 DB 實(shí)例都可以稱(chēng)為“會(huì)話”.
會(huì)話的狀態(tài)信息主要存儲(chǔ)在 statement 當(dāng)中的,所以在克隆 DB 時(shí),很重要的一環(huán)就是完成對(duì) 其中 statement 部分的創(chuàng)建/克隆.
該流程對(duì)應(yīng)的方法為 DB.getInstance 方法,主要通過(guò) DB 中的 clone 字段來(lái)判斷當(dāng)前是首次從始祖 DB 中執(zhí)行克隆操作還是在一個(gè)會(huì)話的基礎(chǔ)上克隆出一個(gè)新的會(huì)話實(shí)例,對(duì)應(yīng)的源碼展示如下:
func (db *DB) getInstance() *DB { if db.clone > 0 { tx := &DB{Config: db.Config, Error: db.Error} // 倘若是首次對(duì) db 進(jìn)行 clone,則需要構(gòu)造出一個(gè)新的 statement 實(shí)例 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), } // 倘若已經(jīng) db clone 過(guò)了,則還需要 clone 原先的 statement } else { // with clone statement tx.Statement = db.Statement.clone() tx.Statement.DB = tx } return tx } return db}
圖片
在 prepare 預(yù)處理模式下,DB 中連接池 connPool 的實(shí)現(xiàn)類(lèi)為 PreparedStmtDB. 定義該類(lèi)的目的是為了使用 database/sql 標(biāo)準(zhǔn)庫(kù)中的 prepare 能力,完成預(yù)處理狀態(tài) statement 的構(gòu)造和復(fù)用.
PreparedStmtDB 的類(lèi)定義如下:
// prepare 模式下的 connPool 實(shí)現(xiàn)類(lèi). type PreparedStmtDB struct { // 各 stmt 實(shí)例. 其中 key 為 sql 模板,stmt 是對(duì)封 database/sql 中 *Stmt 的封裝 Stmts map[string]*Stmt // ... Mux *sync.RWMutex // 內(nèi)置的 ConnPool 字段通常為 database/sql 中的 *DB ConnPool}Stmt 類(lèi)是 gorm 框架對(duì) database/sql 標(biāo)準(zhǔn)庫(kù)下 Stmt 類(lèi)的簡(jiǎn)單封裝,兩者區(qū)別并不大:
type Stmt struct { // database/sql 標(biāo)準(zhǔn)庫(kù)下的 statement *sql.Stmt // 是否處于事務(wù) Transaction bool // 標(biāo)識(shí)當(dāng)前 stmt 是否已初始化完成 prepared chan struct{} prepareErr error}接下來(lái)介紹的是,gorm 框架執(zhí)行 crud 操作邏輯時(shí)使用到的執(zhí)行器 processor,針對(duì) crud 操作的處理函數(shù)會(huì)以 list 的形式聚合在對(duì)應(yīng)類(lèi)型 processor 的 fns 字段當(dāng)中.
type callbacks struct { // 對(duì)應(yīng)存儲(chǔ)了 crud 等各類(lèi)操作對(duì)應(yīng)的執(zhí)行器 processor // query -> query processor // create -> create processor // update -> update processor // delete -> delete processor processors map[string]*processor}各類(lèi) processor 的初始化是通過(guò) initializeCallbacks 方法完成,該方法的調(diào)用入口在本文 3.1 小節(jié)的 gorm.Open 方法中.
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}, }, }}后續(xù)在請(qǐng)求執(zhí)行過(guò)程中,會(huì)根據(jù) crud 的類(lèi)型,從 callbacks 中獲取對(duì)應(yīng)類(lèi)型的 processor. 比如一筆查詢(xún)操作,會(huì)通過(guò) callbacks.Query() 方法獲取對(duì)應(yīng)的 processor:
func (cs *callbacks) Query() *processor { return cs.processors["query"]}執(zhí)行器 processor 具體的類(lèi)定義如下,其中核心字段包括:
? db:從屬的 gorm.DB 實(shí)例
? Clauses:根據(jù) crud 類(lèi)型確定的 SQL 格式模板,后續(xù)用于拼接生成 sql
? fns:對(duì)應(yīng)于 crud 類(lèi)型的執(zhí)行函數(shù)鏈
type processor struct { // 從屬的 DB 實(shí)例 db *DB // 拼接 sql 時(shí)的關(guān)鍵字順序. 比如 query 類(lèi),固定為 SELECT,FROM,WHERE,GROUP BY, ORDER BY, LIMIT, FOR Clauses []string // 對(duì)應(yīng)于 crud 類(lèi)型的執(zhí)行函數(shù)鏈 fns []func(*DB) callbacks []*callback}
圖片
所有請(qǐng)求遵循的處理思路都是,首先根據(jù)其從屬的 crud 類(lèi)型,找到對(duì)應(yīng)的 processor,然后調(diào)用 processor 的 Execute 方法,執(zhí)行該 processor 下的 fns 函數(shù)鏈.
這一點(diǎn),在接下來(lái) 4、5、 6、 7 章中介紹的 crud 流程都是如此.
// 通用的 processor 執(zhí)行函數(shù),其中對(duì)應(yīng)于 crud 的核心操作都被封裝在 processor 對(duì)應(yīng)的 fns list 當(dāng)中了func (p *processor) Execute(db *DB) *DB { // call scopes var ( // ... stmt = db.Statement // ... ) if len(stmt.BuildClauses) == 0 { // 根據(jù) crud 類(lèi)型,對(duì) buildClauses 進(jìn)行復(fù)制,用于后續(xù)的 sql 拼接 stmt.BuildClauses = p.Clauses // ... } // ... // dest 和 model 相互賦值 if stmt.Model == nil { stmt.Model = stmt.Dest } else if stmt.Dest == nil { stmt.Dest = stmt.Model } // 解析 model,獲取對(duì)應(yīng)表的 schema 信息 if stmt.Model != nil { // ... } // 處理 dest 信息,將其添加到 stmt 當(dāng)中 if stmt.Dest != nil { // ... } // 執(zhí)行一系列的 callback 函數(shù),其中最核心的 create/query/update/delete 操作都被包含在其中了. 還包括了一系列前、后處理函數(shù),具體可見(jiàn)第 3 章 for _, f := range p.fns { f(db) } //... return db}在 Execute 方法中,還有一項(xiàng)很重要的事情,是根據(jù) crud 的類(lèi)型,獲取 sql 拼接格式 clauses,將其賦值到該 processor 的 BuildClauses 字段當(dāng)中. crud 各類(lèi) clauses 格式展示如下:
var ( createClauses = []string{"INSERT", "VALUES", "ON CONFLICT"} queryClauses = []string{"SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR"} updateClauses = []string{"UPDATE", "SET", "WHERE"} deleteClauses = []string{"DELETE", "FROM", "WHERE"})接下來(lái)要介紹的是 gorm 框架中的條件 Clause. 一條執(zhí)行 sql 中,各個(gè)部分都屬于一個(gè) clause,比如一條 SELECT * FROM reward WHERE id < 10 ORDER by id 的 SQL,其中就包含了 SELECT、FROM、WHERE 和 ORDER 四個(gè) clause.
當(dāng)使用方通過(guò)鏈?zhǔn)讲僮骺寺?DB時(shí),對(duì)應(yīng)追加的狀態(tài)信息就會(huì)生成一個(gè)新的 clause,追加到 statement 對(duì)應(yīng)的 clauses 集合當(dāng)中. 當(dāng)請(qǐng)求實(shí)際執(zhí)行時(shí),會(huì)取出 clauses 集合,拼接生成完整的 sql 用于執(zhí)行.
條件 clause 本身是個(gè)抽象的 interface,定義如下:
// Interface clause interfacetype Interface interface { // clause 名稱(chēng) Name() string // 生成對(duì)應(yīng)的 sql 部分 Build(Builder) // 和同類(lèi) clause 合并 MergeClause(*Clause)}不同的 clause 有不同的實(shí)現(xiàn)類(lèi),我們以 SELECT 為例進(jìn)行展示:
type Select struct { // 使用使用 distinct 模式 Distinct bool // 是否 select 查詢(xún)指定的列,如 select id,name Columns []Column Expression Expression}func (s Select) Name() string { return "SELECT"}func (s Select) Build(builder Builder) { // select 查詢(xún)指定的列 if len(s.Columns) > 0 { if s.Distinct { builder.WriteString("DISTINCT ") } // 將指定列追加到 sql 語(yǔ)句中 for idx, column := range s.Columns { if idx > 0 { builder.WriteByte(',') } builder.WriteQuoted(column) } // 不查詢(xún)指定列,則使用 select * } else { builder.WriteByte('*') }}拼接 sql 是通過(guò)調(diào)用 Statement.Build 方法來(lái)實(shí)現(xiàn)的,入?yún)?duì)應(yīng)的是 crud 中某一類(lèi) processor 的 BuildClauses.
func (stmt *Statement) Build(clauses ...string) { var firstClauseWritten bool for _, name := range clauses { if c, ok := stmt.Clauses[name]; ok { if firstClauseWritten { stmt.WriteByte(' ') } firstClauseWritten = true if b, ok := stmt.DB.ClauseBuilders[name]; ok { b(c, stmt) } else { c.Build(stmt) } } }}以 query 查詢(xún)類(lèi)為例,會(huì)遵循 "SELECT"->"FROM"->"WHERE"->"GROUP BY"->"ORDER BY"->"LIMIT"->"FOR" 的順序,依次從 statement 中獲取對(duì)應(yīng)的 clause,通過(guò)調(diào)用 clause.Build 方法,將 sql 本文組裝到 statement 的 SQL 字段中.
以 query 流程為例,拼接 sql 的流程入口可以參見(jiàn) 4.3 小節(jié)代碼展示當(dāng)中的 BuildQuerySQL(...) 方法.
圖片
本章中,我們將會(huì)以 gorm.Open 方法作為入口,詳細(xì)展開(kāi)創(chuàng)建 gorm.DB 實(shí)例的源碼細(xì)節(jié).
gorm.Open 方法是創(chuàng)建 DB 實(shí)例的入口方法,其中包含如下幾項(xiàng)核心步驟:
func Open(dialector Dialector, opts ...Option) (db *DB, err error) { config := &Config{} // ... // 表、列命名策略 if config.NamingStrategy == nil { config.NamingStrategy = schema.NamingStrategy{IdentifierMaxLength: 64} // Default Identifier length is 64 } // ... // 連接器 if dialector != nil { config.Dialector = dialector } // ... db = &DB{Config: config, clone: 1} // 初始化 callback 當(dāng)中的各個(gè) processor db.callbacks = initializeCallbacks(db) // ... if config.Dialector != nil { // 在其中會(huì)對(duì) crud 各個(gè)方法的 callback 方法進(jìn)行注冊(cè) // 會(huì)對(duì) db.connPool 進(jìn)行初始化,通常情況下是 database/sql 庫(kù)下 *sql.DB 的類(lèi)型 err = config.Dialector.Initialize(db) // ... } // 是否啟用 prepare 模式 if config.PrepareStmt { preparedStmt := NewPreparedStmtDB(db.ConnPool) db.cacheStore.Store(preparedStmtDBKey, preparedStmt) // 倘若啟用了 prepare 模式,會(huì)對(duì) conn 進(jìn)行替換 db.ConnPool = preparedStmt } // 構(gòu)造一個(gè) statement 用于存儲(chǔ)處理鏈路中的一些狀態(tài)信息 db.Statement = &Statement{ DB: db, ConnPool: db.ConnPool, Context: context.Background(), Clauses: map[string]clause.Clause{}, } // 倘若未禁用 AutomaticPing, if err == nil && !config.DisableAutomaticPing { if pinger, ok := db.ConnPool.(interface{ Ping() error }); ok { err = pinger.Ping() } } // ... return}
mysql 是我們常用的數(shù)據(jù)庫(kù),對(duì)應(yīng)于 mysql 版本的 dialector 實(shí)現(xiàn)類(lèi)位于 github.com/go-sql-driver/mysql 包下. 使用方可以通過(guò) Open 方法,將傳入的 dsn 解析成配置,然后返回 mysql 版本的 Dialector 實(shí)例.
package mysqlfunc Open(dsn string) gorm.Dialector { dsnConf, _ := mysql.ParseDSN(dsn) return &Dialector{Config: &Config{DSN: dsn, DSNConfig: dsnConf}}}通過(guò) Dialector.Initialize 方法完成連接器初始化操作,其中也會(huì)涉及到對(duì)連接池 connPool 的初構(gòu)造,并通過(guò) callbacks.RegisterDefaultCallbacks 方法完成 crud 四類(lèi) processor 當(dāng)中 fns 的注冊(cè)操作:
import( "github.com/go-sql-driver/mysql")func (dialector Dialector) Initialize(db *gorm.DB) (err error) { if dialector.DriverName == "" { dialector.DriverName = "mysql" } // connPool 初始化 if dialector.Conn != nil { db.ConnPool = dialector.Conn } else { db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN) if err != nil { return err } } // ... // register callbacks callbackConfig := &callbacks.Config{ CreateClauses: CreateClauses, QueryClauses: QueryClauses, UpdateClauses: UpdateClauses, DeleteClauses: DeleteClauses, } // ...完成 crud 類(lèi)操作 callback 函數(shù)的注冊(cè) callbacks.RegisterDefaultCallbacks(db, callbackConfig) // ... return}
圖片
對(duì)應(yīng)于 crud 四類(lèi) processor,注冊(cè)的函數(shù)鏈 fns 的內(nèi)容和順序是固定的,展示如上圖. 相應(yīng)的源碼展示如下,對(duì)應(yīng)的方法為 RegisterDefaultCallbacks(...):
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) { // ... // 創(chuàng)建類(lèi) create processor createCallback := db.Callback().Create() createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction) createCallback.Register("gorm:before_create", BeforeCreate) createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true)) createCallback.Register("gorm:create", Create(config)) createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true)) createCallback.Register("gorm:after_create", AfterCreate) createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) createCallback.Clauses = config.CreateClauses // 查詢(xún)類(lèi) query processor queryCallback := db.Callback().Query() queryCallback.Register("gorm:query", Query) queryCallback.Register("gorm:preload", Preload) queryCallback.Register("gorm:after_query", AfterQuery) queryCallback.Clauses = config.QueryClauses // 刪除類(lèi) delete processor deleteCallback := db.Callback().Delete() deleteCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction) deleteCallback.Register("gorm:before_delete", BeforeDelete) deleteCallback.Register("gorm:delete_before_associations", DeleteBeforeAssociations) deleteCallback.Register("gorm:delete", Delete(config)) deleteCallback.Register("gorm:after_delete", AfterDelete)deleteCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) deleteCallback.Clauses = config.DeleteClauses // 更新類(lèi) update processor updateCallback := db.Callback().Update() updateCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction) updateCallback.Register("gorm:setup_reflect_value", SetupUpdateReflectValue) updateCallback.Register("gorm:before_update", BeforeUpdate) updateCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(false)) updateCallback.Register("gorm:update", Update(config)) updateCallback.Register("gorm:save_after_associations", SaveAfterAssociations(false)) updateCallback.Register("gorm:after_update", AfterUpdate) updateCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) updateCallback.Clauses = config.UpdateClauses // row 類(lèi) rowCallback := db.Callback().Row() rowCallback.Register("gorm:row", RowQuery) rowCallback.Clauses = config.QueryClauses // raw 類(lèi) rawCallback := db.Callback().Raw() rawCallback.Register("gorm:raw", RawExec) rawCallback.Clauses = config.QueryClauses}注冊(cè)某個(gè)特定 fn 函數(shù)的入口是 processor.Register 方法,對(duì)應(yīng)的核心源碼鏈路展示如下:
func (p *processor) Register(name string, fn func(*DB)) error { return (&callback{processor: p}).Register(name, fn)}func (c *callback) Register(name string, fn func(*DB)) error { c.name = name c.handler = fn c.processor.callbacks = append(c.processor.callbacks, c) return c.processor.compile()}func (p *processor) compile() (err error) { var callbacks []*callback for _, callback := range p.callbacks { if callback.match == nil || callback.match(p.db) { callbacks = append(callbacks, callback) } } p.callbacks = callbacks if p.fns, err = sortCallbacks(p.callbacks); err != nil { p.db.Logger.Error(context.Background(), "Got error when compile callbacks, got %v", err) } return}func sortCallbacks(cs []*callback) (fns []func(*DB), err error) { var ( names, sorted []string sortCallback func(*callback) error ) // ... sortCallback = func(c *callback) error { // ... // if current callback haven't been sorted, append it to last if getRIndex(sorted, c.name) == -1 { sorted = append(sorted, c.name) } return nil } for _, c := range cs { if err = sortCallback(c); err != nil { return } } for _, name := range sorted { if idx := getRIndex(names, name); !cs[idx].remove { fns = append(fns, cs[idx].handler) } } return}
圖片
接下來(lái)以 db.First 方法作為入口,展示數(shù)據(jù)庫(kù)查詢(xún)的方法鏈路:
在 db.First 方法當(dāng)中:
? 遵循 First 的語(yǔ)義,通過(guò) limit 和 order 追加 clause,限制只取滿(mǎn)足條件且主鍵最小的一筆數(shù)據(jù)
? 追加用戶(hù)傳入的一系列 condition,進(jìn)行 clause 追加
? 在 First、Take、Last 等方法中,會(huì)設(shè)置 RaiseErrorOnNotFound 標(biāo)識(shí)為 true,倘若未找到記錄,則會(huì)拋出 ErrRecordNotFound 錯(cuò)誤
var ErrRecordNotFound = logger.ErrRecordNotFound? 設(shè)置 statement 中的 dest 為用戶(hù)傳入的 dest,作為反序列化響應(yīng)結(jié)果的對(duì)象實(shí)例
? 獲取 query 類(lèi)型的 processor,調(diào)用 Execute 方法執(zhí)行其中的 fn 函數(shù)鏈,完成 query 操作
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) { // order by id limit 1 tx = db.Limit(1).Order(clause.OrderByColumn{ Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, }) // append clauses if len(conds) > 0 { if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 { tx.Statement.AddClause(clause.Where{Exprs: exprs}) } } // set RaiseErrorOnNotFound tx.Statement.RaiseErrorOnNotFound = true // set dest tx.Statement.Dest = dest // execute ... return tx.callbacks.Query().Execute(tx)}
圖片
執(zhí)行查詢(xún)類(lèi)操作時(shí),通常會(huì)通過(guò)鏈?zhǔn)秸{(diào)用的方式,傳入一些查詢(xún)限制條件,比如 Where、Group By、Order、Limit 之類(lèi). 我們以 Limit 為例,進(jìn)行展開(kāi)介紹:
func (db *DB) Limit(limit int) (tx *DB) { tx = db.getInstance() tx.Statement.AddClause(clause.Limit{Limit: &limit}) return}func (stmt *Statement) AddClause(v clause.Interface) { // ... name := v.Name() c := stmt.Clauses[name] c.Name = name v.MergeClause(&c) stmt.Clauses[name] = c }
圖片
在 query 類(lèi)型 processor 的 fns 函數(shù)鏈中,最主要的函數(shù)是 Query,其中涉及的核心步驟包括:
? 調(diào)用 BuildQuerySQL(...) 方法,根據(jù)傳入的 clauses 組裝生成 sql
? 調(diào)用 connPool.QueryContext(...) ,完成查詢(xún)類(lèi) sql 的執(zhí)行,返回查到的行數(shù)據(jù) rows(非 prepare 模式下,此處會(huì)對(duì)接 database/sql 庫(kù),走到 sql.DB.QueryContext(...) 方法中)
? 調(diào)用 gorm.Scan() 方法,將結(jié)果數(shù)據(jù)反序列化到 statement 的 dest 當(dāng)中
func Query(db *gorm.DB) { if db.Error == nil { // 拼接生成 sql BuildQuerySQL(db) if !db.DryRun && db.Error == nil { rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if err != nil { db.AddError(err) return } defer func() { db.AddError(rows.Close()) }() gorm.Scan(rows, db, 0) } }}
接下來(lái)展示一下,gorm.Scan() 方法,其作用是將查詢(xún)結(jié)果數(shù)據(jù)反序列化到 dest 當(dāng)中:
對(duì)應(yīng)源碼展示如下:
// Scan 方法將 rows 中的數(shù)據(jù)掃描解析到 db statement 中的 dest 當(dāng)中// 其中 rows 通常為 database/sql 下的 *Rows 類(lèi)型// 掃描數(shù)據(jù)的核心在于調(diào)用了 rows.Scan 方法func Scan(rows Rows, db *DB, mode ScanMode) { var ( columns, _ = rows.Columns() values = make([]interface{}, len(columns)) initialized = mode&ScanInitialized != 0 update = mode&ScanUpdate != 0 onConflictDonothing = mode&ScanOnConflictDoNothing != 0 ) // 影響的行數(shù) db.RowsAffected = 0 // 根據(jù) dest 類(lèi)型進(jìn)行斷言分配 switch dest := db.Statement.Dest.(type) { case map[string]interface{}, *map[string]interface{}: if initialized || rows.Next() { // ... db.RowsAffected++ // 掃描數(shù)據(jù)的核心在于,調(diào)用 rows db.AddError(rows.Scan(values...)) // ... } case *[]map[string]interface{}: columnTypes, _ := rows.ColumnTypes() for initialized || rows.Next() { // ... db.RowsAffected++ db.AddError(rows.Scan(values...)) mapValue := map[string]interface{}{} scanIntoMap(mapValue, values, columns) *dest = append(*dest, mapValue) } case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64, *uintptr, *float32, *float64, *bool, *string, *time.Time, *sql.NullInt32, *sql.NullInt64, *sql.NullFloat64, *sql.NullBool, *sql.NullString, *sql.NullTime: for initialized || rows.Next() { initialized = false db.RowsAffected++ db.AddError(rows.Scan(dest)) } default: // ... // 根據(jù) dest 類(lèi)型進(jìn)行前處理 ... db.AddError(rows.Scan(dest)) // ... } // 倘若 rows 中存在錯(cuò)誤,需要拋出 if err := rows.Err(); err != nil && err != db.Error { db.AddError(err) } // 在 first、last、take 模式下,RaiseErrorOnNotFound 標(biāo)識(shí)為 true,在沒(méi)有查找到數(shù)據(jù)時(shí),會(huì)拋出 ErrRecordNotFound 錯(cuò)誤 if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound && db.Error == nil { db.AddError(ErrRecordNotFound) }}
圖片
本章以 db.Create(...) 方法為入口,展開(kāi)介紹一下創(chuàng)建數(shù)據(jù)記錄的流程.
創(chuàng)建數(shù)據(jù)記錄操作主要通過(guò)調(diào)用 gorm.DB 的 Create 方法完成,其包括如下核心步驟:
// Create inserts value, returning the inserted data's primary key in value's idfunc (db *DB) Create(value interface{}) (tx *DB) { // ... // 克隆 db 會(huì)話實(shí)例 tx = db.getInstance() // 設(shè)置 dest tx.Statement.Dest = value // 執(zhí)行 create processor return tx.callbacks.Create().Execute(tx)}
在 create 類(lèi)型 processor 的 fns 函數(shù)鏈中,最主要的執(zhí)行函數(shù)就是 Create,其中核心步驟包括:
? 調(diào)用 statement.Build(...) 方法,生成 sql
? 調(diào)用 connPool.ExecContext(...) 方法,請(qǐng)求 mysql 服務(wù)端執(zhí)行 sql(默認(rèn)情況下,此處會(huì)使用 database/sql 標(biāo)準(zhǔn)庫(kù)的 db.ExecContext(...) 方法)
? 調(diào)用 result.RowsAffected() ,獲取到本次創(chuàng)建操作影響的數(shù)據(jù)行數(shù)
// Create create hookfunc Create(config *Config) func(db *gorm.DB) { supportReturning := utils.Contains(config.CreateClauses, "RETURNING") return func(db *gorm.DB) { // 生成 sql if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(180) db.Statement.AddClauseIfNotExists(clause.Insert{}) db.Statement.AddClause(ConvertToCreateValues(db.Statement)) db.Statement.Build(db.Statement.BuildClauses...) } // ... 執(zhí)行 sql result, err := db.Statement.ConnPool.ExecContext( db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars..., ) // ... 獲取影響的行數(shù) db.RowsAffected, _ = result.RowsAffected() // ... }}
圖片
接下來(lái)是數(shù)據(jù)記錄刪除流程,以 db.Delete 方法作為走讀入口:
在 db.Delete 方法中,核心步驟包括:
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB) { tx = db.getInstance() if len(conds) > 0 { if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 { tx.Statement.AddClause(clause.Where{Exprs: exprs}) } } tx.Statement.Dest = value return tx.callbacks.Delete().Execute(tx)}在 delete 類(lèi)型的 processor 的 fns 函數(shù)鏈中,最核心的函數(shù)是 Delete,其中的核心步驟包括:
var ErrMissingWhereClause = errors.New("WHERE conditions required")func Delete(config *Config) func(db *gorm.DB) { supportReturning := utils.Contains(config.DeleteClauses, "RETURNING") return func(db *gorm.DB) { // ... if db.Statement.Schema != nil { for _, c := range db.Statement.Schema.DeleteClauses { db.Statement.AddClause(c) } } // 生成 sql if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(100) db.Statement.AddClauseIfNotExists(clause.Delete{}) // ... db.Statement.AddClauseIfNotExists(clause.From{}) db.Statement.Build(db.Statement.BuildClauses...) } // ... checkMissingWhereConditions(db) // ... if !db.DryRun && db.Error == nil { // ... result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if db.AddError(err) == nil { db.RowsAffected, _ = result.RowsAffected() } } }}
checkMissingWhereConditions 方法的源碼如下:
func checkMissingWhereConditions(db *gorm.DB) { // 倘若 AllowGlobalUpdate 標(biāo)識(shí)不為 true 且 error 為空,則需要對(duì) where 條件進(jìn)行校驗(yàn) if !db.AllowGlobalUpdate && db.Error == nil { where, withCondition := db.Statement.Clauses["WHERE"] // ... // 不存在 where 條件,則需要拋出錯(cuò)誤 if !withCondition { db.AddError(gorm.ErrMissingWhereClause) } return }}
圖片
下面展示的是通過(guò) gorm 更新數(shù)據(jù)的流程,以 db.Update(...) 方法作為源碼走讀的入口:
在 db.Update 方法中,核心步驟包括:
? 通過(guò) db.getInstance() 方法獲取 db 的克隆實(shí)例
? 設(shè)置 statement dest 為使用方傳入的 value
? 獲取 update 類(lèi)型的 processor
? 執(zhí)行 processor.Execute(...) 方法,遍歷調(diào)用 fns 函數(shù)鏈
func (db *DB) Updates(values interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Dest = values return tx.callbacks.Update().Execute(tx)}在 update 類(lèi)型 processor 的 fns 函數(shù)鏈中,最核心的函數(shù)就是 Update,其中核心步驟包括:
? 調(diào)用 statement.Build(...) 方法,生成 sql
? 和 Delete 流程類(lèi)似,倘若未啟用 AllowGlobalUpdate 模式,則會(huì)校驗(yàn)使用方是否設(shè)置了 where 條件,未設(shè)置會(huì)拋出 gorm.ErrMissingWhereClause 錯(cuò)誤
? 調(diào)用 connPool.ExecContext(...) 方法,執(zhí)行 sql(默認(rèn)情況下,此處會(huì)使用 database/sql 標(biāo)準(zhǔn)庫(kù)的 db.ExecContext(...) 方法)
? 調(diào)用 result.RowsAffected() 方法,獲取到本次更新操作影響的行數(shù)
// Update update hookfunc Update(config *Config) func(db *gorm.DB) { supportReturning := utils.Contains(config.UpdateClauses, "RETURNING") return func(db *gorm.DB) { // ... if db.Statement.Schema != nil { for _, c := range db.Statement.Schema.UpdateClauses { db.Statement.AddClause(c) } } // 生成 sql if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(180) db.Statement.AddClauseIfNotExists(clause.Update{}) // ... db.Statement.Build(db.Statement.BuildClauses...) } // ... 校驗(yàn) where 條件 checkMissingWhereConditions(db) if !db.DryRun && db.Error == nil { // ... 執(zhí)行 sql result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if db.AddError(err) == nil { // 獲取影響的行數(shù) db.RowsAffected, _ = result.RowsAffected() } } }}
圖片
通過(guò) gorm 框架同樣能夠很方便地使用事務(wù)相關(guān)的功能:
? 調(diào)用 db.Transaction(...) 方法
? 傳入閉包函數(shù) fc,其中入?yún)?tx 為帶有事務(wù)會(huì)話屬性的 db 實(shí)例,后續(xù)事務(wù)內(nèi)所有執(zhí)行操作都需要圍繞這個(gè) tx 展開(kāi)
? 可以使用該 tx 實(shí)例完成事務(wù)的提交 tx.Commit() 和回滾 tx.Rollback() 操作
db.Transaction(...) 方法是啟動(dòng)事務(wù)的入口:
? 首先會(huì)調(diào)用 db.Begin(...) 方法啟動(dòng)事務(wù),此時(shí)會(huì)克隆出一個(gè)帶有事務(wù)屬性的 DB 會(huì)話實(shí)例:tx
? 以 tx 為入?yún)ⅲ{(diào)用使用方傳入的閉包函數(shù) fc(tx)
? 倘若 fc 執(zhí)行成功,則自動(dòng)為用戶(hù)執(zhí)行 tx.Commit() 操作
? 倘若 fc 執(zhí)行出錯(cuò)或者發(fā)生 panic,則會(huì) defer 保證執(zhí)行 tx.Rollback() 操作
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) { panicked := true if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil { // ... } else { // 開(kāi)啟事務(wù) tx := db.Begin(opts...) if tx.Error != nil { return tx.Error } defer func() { // 倘若發(fā)生錯(cuò)誤或者 panic,則進(jìn)行 rollback 回滾 if panicked || err != nil { tx.Rollback() } }() // 執(zhí)行事務(wù)內(nèi)的邏輯 if err = fc(tx); err == nil { panicked = false // 指定成功會(huì)進(jìn)行 commit 操作 return tx.Commit().Error } } panicked = false return}
對(duì)于 DB.Begin() 方法,在默認(rèn)模式下會(huì)使用 database/sql 庫(kù)下的 sql.DB.BeginTx 方法創(chuàng)建出一個(gè) sql.Tx 對(duì)象,將其賦給當(dāng)前事務(wù)會(huì)話 DB 的 statement.ConnPool 字段,以供后續(xù)使用:
// Begin begins a transaction with any transaction options optsfunc (db *DB) Begin(opts ...*sql.TxOptions) *DB { var ( // clone statement tx = db.getInstance().Session(&Session{Context: db.Statement.Context, NewDB: db.clone == 1}) opt *sql.TxOptions err error ) if len(opts) > 0 { opt = opts[0] } switch beginner := tx.Statement.ConnPool.(type) { // 標(biāo)準(zhǔn)模式,會(huì)走到 sql.DB.BeginTX 方法 case TxBeginner: // 創(chuàng)建好的 tx 賦給 statment.ConnPool tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) // prepare 模式,會(huì)走到 PreparedStmtDB.BeginTx 方法中 case ConnPoolBeginner: // 創(chuàng)建好的 tx 賦給 statment.ConnPool tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) default: err = ErrInvalidTransaction } if err != nil { tx.AddError(err) } return tx}
事務(wù)的提交和回滾操作,會(huì)執(zhí)行 statement 中的 connPool 的 Commit 和 Rollback 方法完成:
? 執(zhí)行事務(wù)提交操作:
// Commit commits the changes in a transactionfunc (db *DB) Commit() *DB { // 默認(rèn)情況下,此處的 ConnPool 實(shí)現(xiàn)類(lèi)為 database/sql.Tx if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil && !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Commit()) } else { db.AddError(ErrInvalidTransaction) } return db}? 執(zhí)行事務(wù)回滾操作:
// Rollback rollbacks the changes in a transactionfunc (db *DB) Rollback() *DB { // 默認(rèn)情況下,此處的 ConnPool 實(shí)現(xiàn)類(lèi)為 database/sql.Tx if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil { if !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Rollback()) } } else { db.AddError(ErrInvalidTransaction) } return db}
圖片
倘若創(chuàng)建 gorm.DB 時(shí),倘若在 Config 中設(shè)置了 PrepareStmt 標(biāo)識(shí),則代表后續(xù)會(huì)啟用 prepare 預(yù)處理模式. 次吃,在執(zhí)行 query 或者 exec 操作時(shí),使用的 ConnPool 的實(shí)現(xiàn)版本是 PreparedStmtDB,執(zhí)行時(shí)會(huì)拆分為兩個(gè)步驟:
? 通過(guò) PreparedStmtDB.prepare(...) 操作創(chuàng)建/復(fù)用 stmt,后續(xù)相同 sql 模板可以復(fù)用此 stmt
? 通過(guò) stmt.Query(...)/Exec(...) 執(zhí)行 sql
在 PreparedStmtDB.prepare 方法中,會(huì)通過(guò)加鎖 double check 的方式,創(chuàng)建或復(fù)用 sql 模板對(duì)應(yīng)的 stmt. 創(chuàng)建 stmt 的操作通過(guò)調(diào)用 conn.PrepareContext 方法完成.(通常此處的 conn 為 database/sql 庫(kù)下的 sql.DB)
PreparedStmtDB.prepare 方法核心流程梳理如下:
? 加讀鎖,然后以 sql 模板為 key,嘗試從 db.Stmts map 中獲取 stmt 復(fù)用
? 倘若 stmt 不存在,則加寫(xiě)鎖 double check
? 調(diào)用 conn.PrepareContext(...) 方法,創(chuàng)建新的 stmt,并存放到 map 中供后續(xù)復(fù)用
完整的代碼和對(duì)應(yīng)的注釋展示如下:
func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) { db.Mux.RLock() // 以 sql 模板為 key,優(yōu)先復(fù)用已有的 stmt if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) { db.Mux.RUnlock() // 并發(fā)場(chǎng)景下,只允許有一個(gè) goroutine 完成 stmt 的初始化操作 <-stmt.prepared if stmt.prepareErr != nil { return Stmt{}, stmt.prepareErr } return *stmt, nil } db.Mux.RUnlock() // 加鎖 double check,確認(rèn)未完成 stmt 初始化則執(zhí)行初始化操作 db.Mux.Lock() // double check if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) { db.Mux.Unlock() // wait for other goroutines prepared <-stmt.prepared if stmt.prepareErr != nil { return Stmt{}, stmt.prepareErr } return *stmt, nil } // 創(chuàng)建 stmt 實(shí)例,并添加到 stmts map 中 cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})} db.Stmts[query] = &cacheStmt // 此時(shí)可以提前解鎖是因?yàn)檫€通過(guò) channel 保證了其他使用者會(huì)阻塞等待初始化操作完成 db.Mux.Unlock() // 所有工作執(zhí)行完之后會(huì)關(guān)閉 channel,喚醒其他阻塞等待使用 stmt 的 goroutine defer close(cacheStmt.prepared) // 調(diào)用 *sql.DB 的 prepareContext 方法,創(chuàng)建真正的 stmt stmt, err := conn.PrepareContext(ctx, query) if err != nil { cacheStmt.prepareErr = err db.Mux.Lock() delete(db.Stmts, query) db.Mux.Unlock() return Stmt{}, err } db.Mux.Lock() cacheStmt.Stmt = stmt db.PreparedSQL = append(db.PreparedSQL, query) db.Mux.Unlock() return cacheStmt,nil}
在 prepare 模式下,查詢(xún)操作通過(guò) PreparedStmtDB.QueryContext(...) 方法實(shí)現(xiàn). 首先通過(guò) PreparedStmtDB.prepare(...) 方法嘗試復(fù)用 stmt,然后調(diào)用 stmt.QueryContext(...) 執(zhí)行查詢(xún)操作.
此處 stm.QueryContext(...) 方法本質(zhì)上會(huì)使用 database/sql 中的 sql.Stmt 完成任務(wù).
func (db *PreparedStmtDB) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) { stmt, err := db.prepare(ctx, db.ConnPool, false, query) if err == nil { rows, err = stmt.QueryContext(ctx, args...) if err != nil { db.Mux.Lock() defer db.Mux.Unlock() go stmt.Close() delete(db.Stmts, query) } } return rows, err}
在 prepare 模式下,執(zhí)行操作通過(guò) PreparedStmtDB.ExecContext(...) 方法實(shí)現(xiàn). 首先通過(guò) PreparedStmtDB.prepare(...) 方法嘗試復(fù)用 stmt,然后調(diào)用 stmt.ExecContext(...) 執(zhí)行查詢(xún)操作.
此處 stm.ExecContext(...) 方法本質(zhì)上會(huì)使用 database/sql 中的 sql.Stmt 完成任務(wù).
func (db *PreparedStmtDB) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) { stmt, err := db.prepare(ctx, db.ConnPool, false, query) if err == nil { result, err = stmt.ExecContext(ctx, args...) if err != nil { db.Mux.Lock() defer db.Mux.Unlock() go stmt.Close() delete(db.Stmts, query) } } return result, err}
本期主要和大家一起解析了 gorm 框架的底層實(shí)現(xiàn)原理.
通篇學(xué)習(xí)下來(lái),相信大家也能夠看出,gorm 框架名副其實(shí),正是基于 orm 的思想,為使用方屏蔽了大量和 sql、db 有關(guān)的細(xì)節(jié),讓使用方能夠像操作對(duì)象一樣完成和數(shù)據(jù)庫(kù)的交互操作.
本文鏈接:http://m.www897cc.com/showinfo-26-64098-0.htmlGorm 框架原理&源碼解析
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com