自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Gorm 框架原理&源碼解析

開發(fā) 前端
gorm 框架通過一個(gè) gorm.DB 實(shí)例來指代我們所操作的數(shù)據(jù)庫. 使用 gorm 的第一步就是要通過 Open 方法創(chuàng)建出一個(gè) gorm.DB 實(shí)例,其中首個(gè)入?yún)檫B接器 dialector,本身是個(gè)抽象的 interface,其實(shí)現(xiàn)類關(guān)聯(lián)了具體數(shù)據(jù)庫類型.

0 前言

本篇將和大家探討 go 語言中最流行的 orm 框架 ——gorm 的底層實(shí)現(xiàn)原理.

本篇分享內(nèi)容的目錄大綱如下所示:

圖片圖片

1 入口

圖片圖片

gorm 框架是國內(nèi)的大神 jinzhu 基于 go 語言開源實(shí)現(xiàn)的一款數(shù)據(jù)庫 orm 框架. 【gorm】一詞恢弘大氣,前綴 go 代表 go 語言, 后綴 orm 全稱 Object Relation Mapping,指的是使用對象映射的方式,讓使用方能夠像操作本地對象實(shí)例一樣輕松便捷地完成遠(yuǎn)端數(shù)據(jù)庫的操作.

gorm 框架開源地址為: https://github.com/go-gorm/gorm

本期會(huì)涉及到大量 gorm 的源碼走讀環(huán)節(jié),使用的代碼版本為 tag: v.1.25.5

1.1 初始化

gorm 框架通過一個(gè) gorm.DB 實(shí)例來指代我們所操作的數(shù)據(jù)庫. 使用 gorm 的第一步就是要通過 Open 方法創(chuàng)建出一個(gè) gorm.DB 實(shí)例,其中首個(gè)入?yún)檫B接器 dialector,本身是個(gè)抽象的 interface,其實(shí)現(xiàn)類關(guān)聯(lián)了具體數(shù)據(jù)庫類型.

本文將統(tǒng)一以 mysql 為例,注入 gorm.io/driver/mysql 包下定義的 dialector 類.

package mysql


import (
    "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 章中深入源碼底層鏈路.

1.2 po 模型

基于 orm 的思路,與某張數(shù)據(jù)表所關(guān)聯(lián)映射的是 po (persist object)模型.

定義 po 類時(shí),可以通過聲明 TableName 方法,來指定該類對應(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 類時(shí),可以通過組合 gorm.Model 的方式,完成主鍵、增刪改時(shí)間等4列信息的一鍵添加,并且由于聲明了 DeletedAt 字段,gorm 將會(huì)默認(rèn)會(huì)啟動(dòng)軟刪除模式. (有關(guān)軟刪除的內(nèi)容,可參見前文——gorm 框架使用教程)

type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

1.3 查詢

下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)查詢操作的代碼示例. 本文第 4 章會(huì)以 db.First(...) 方法為入口,展開底層源碼鏈路的走讀.

func Test_query(t *testing.T) {
    // 獲取 db 
    db, _ := getDB()


    // 超時(shí)控制
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()


    // 查詢
    var r Reward
    if err := db.WithContext(ctx).First(&r).Error; err != nil {
        t.Error(err)
        return
    }


    t.Logf("reward: %+v", r)
}

1.4 創(chuàng)建

下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)創(chuàng)建操作的代碼示例. 本文第 5 章會(huì)以 db.Create(...) 方法為入口,展開底層源碼鏈路的走讀.

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
    }
}

1.5 刪除

下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)刪除操作的代碼示例. 本文第 6 章會(huì)以 db.Delete(...) 方法為入口,展開底層源碼鏈路的走讀.

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
    }
}

1.6 更新

下面展示的是使用 gorm 進(jìn)行數(shù)據(jù)更新操作的代碼示例. 本文第 7 章會(huì)以 db.Update(...) 方法為入口,展開底層源碼鏈路的走讀.

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
    }
}

1.7 事務(wù)

下面展示的是使用 gorm 開啟事務(wù)的代碼示例. 本文第 8 章會(huì)以 db.Transaction(...) 方法為入口,展開底層源碼鏈路的走讀.

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
    }
}

 

2 核心類

本章中,我會(huì)首先向大家介紹 gorm 框架中各個(gè)核心類的定義.

2.1 數(shù)據(jù)庫

gorm.DB 是 gorm 定義的數(shù)據(jù)庫類. 所有執(zhí)行的數(shù)據(jù)庫的操作都將緊密圍繞這個(gè)類,以鏈?zhǔn)秸{(diào)用的方式展開. 每當(dāng)執(zhí)行過鏈?zhǔn)秸{(diào)用后,新生成的 DB 對象中就存儲(chǔ)了一些當(dāng)前請求特有的狀態(tài)信息,我們把這種對象稱作“會(huì)話”.

DB 類中的核心字段包括:

? Config:用戶自定義的配置項(xiàng)

? Error:一次會(huì)話執(zhí)行過程中遇到的錯(cuò)誤

? RowsAffected:該請求影響的行數(shù)

? Statement:一次會(huì)話的狀態(tài)信息,比如請求和響應(yīng)信息

? clone:會(huì)話被克隆的次數(shù). 倘若 clone = 1,代表是始祖 DB 實(shí)例;倘若 clone > 1,代表是從始祖 DB 克隆出來的會(huì)話

DB 類定義的代碼如下:

// gorm 中定義的數(shù)據(jù)庫類
// 所有 orm 的思想
type DB struct {
    // 配置
    *Config
    // 錯(cuò)誤
    Error        error
    // 影響的行數(shù)
    RowsAffected int64
    // 會(huì)話狀態(tài)信息
    Statement    *Statement
    // 克隆次數(shù)
    clone        int
}

I 錯(cuò)誤處理

DB 類的 AddError 方法,用于在會(huì)話執(zhí)行過程中拋出錯(cuò)誤.

一次會(huì)話在執(zhí)行過程中可能會(huì)遇到多個(gè)錯(cuò)誤,因此會(huì)通過 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
}

II 表名設(shè)置

圖片圖片

請求在執(zhí)行時(shí),需要明確操作的是哪張數(shù)據(jù)表.

使用方可以通過鏈?zhǔn)秸{(diào)用 DB.Table 方法,顯式聲明本次操作所針對的數(shù)據(jù)表,這種方式的優(yōu)先級是最高的.

func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
    tx = db.getInstance()
    // ...
    tx.Statement.Table = name
    // ...
    return
}

在 DB.Table 方法缺省的情況下,gorm 則會(huì)嘗試通過 po 類的 TableName 方法獲取表名.

在 gorm 中聲明了一個(gè) tabler interface:

type Tabler interface {
    TableName() string
}

倘若 po 模型聲明了 TableName 方法,則隱式實(shí)現(xiàn)了該 interface,在處理過程中會(huì)被斷言成 tabler 類型,然后調(diào)用 TableName 方法獲取其表名.

該流程對應(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 table
func 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
}

2.2 會(huì)話狀態(tài)

接下來介紹的是 gorm 中非常核心的 statement 類,里面存儲(chǔ)了一次會(huì)話中包含的狀態(tài)信息,比如請求中的條件、sql 語句拼接格式、響應(yīng)參數(shù)類型、數(shù)據(jù)表的名稱等等.

statement 類中涉及到的各個(gè)核心字段通過下方的代碼和注釋加以介紹:

// Statement statement
type Statement struct {
    // 數(shù)據(jù)庫實(shí)例
    *DB
    // ...
    // 表名
    Table                string
    // 操作的 po 模型
    Model                interface{}
    // ...
    // 處理結(jié)果反序列化到此處
    Dest                 interface{}
    // ...
    // 各種條件語句
    Clauses              map[string]clause.Clause
    
    // ...
    // 是否啟用 distinct 模式
    Distinct             bool
    // select 語句
    Selects              []string // selected columns
    // omit 語句
    Omits                []string // omit columns
    // join 
    Joins                []join
    
    // ...
    // 連接池,通常情況下是 database/sql 庫下的 *DB  類型.  在 prepare 模式為 gorm.PreparedStmtDB
    ConnPool             ConnPool
    // 操作表的概要信息
    Schema               *schema.Schema
    // 上下文,請求生命周期控制管理
    Context              context.Context
    // 在未查找到數(shù)據(jù)記錄時(shí),是否拋出 recordNotFound 錯(cuò)誤
    RaiseErrorOnNotFound bool
    // ...
    // 執(zhí)行的 sql,調(diào)用 state.Build 方法后,會(huì)將 sql 各部分文本依次追加到其中. 具體可見 2.5 小節(jié)
    SQL                  strings.Builder
    // 存儲(chǔ)的變量
    Vars                 []interface{}
    // ...
}

I connPool

這里額外強(qiáng)調(diào)一下 connPool 字段,其含義是連接池,和數(shù)據(jù)庫的交互操作都需要依賴它才得以執(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)類版本:

  • ? **在普通模式下,connPool 的實(shí)現(xiàn)類為 database/sql 庫下的 *DB 類**(詳細(xì)內(nèi)容參見前文——Golang sql 標(biāo)準(zhǔn)庫源碼解析)
  • ? 在 prepare 模式下,connPool 實(shí)現(xiàn)類型為 gorm 中定義的 PreparedStmtDB 類,在本文 2.3 小節(jié)中展開

II db 克隆

此處額外介紹一下 DB 的克隆流程,所有在始祖 DB 基礎(chǔ)上追加狀態(tài)信息,克隆出來的 DB 實(shí)例都可以稱為“會(huì)話”.

會(huì)話的狀態(tài)信息主要存儲(chǔ)在 statement 當(dāng)中的,所以在克隆 DB 時(shí),很重要的一環(huán)就是完成對 其中 statement 部分的創(chuàng)建/克隆.

該流程對應(yīng)的方法為 DB.getInstance 方法,主要通過 DB 中的 clone 字段來判斷當(dāng)前是首次從始祖 DB 中執(zhí)行克隆操作還是在一個(gè)會(huì)話的基礎(chǔ)上克隆出一個(gè)新的會(huì)話實(shí)例,對應(yīng)的源碼展示如下:

func (db *DB) getInstance() *DB {
    if db.clone > 0 {
        tx := &DB{Config: db.Config, Error: db.Error}


        // 倘若是首次對 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 過了,則還需要 clone 原先的 statement
        } else {
            // with clone statement
            tx.Statement = db.Statement.clone()
            tx.Statement.DB = tx
        }


        return tx
    }


    return db
}

 

2.3 預(yù)處理DB

圖片圖片

在 prepare 預(yù)處理模式下,DB 中連接池 connPool 的實(shí)現(xiàn)類為 PreparedStmtDB. 定義該類的目的是為了使用 database/sql 標(biāo)準(zhǔn)庫中的 prepare 能力,完成預(yù)處理狀態(tài) statement 的構(gòu)造和復(fù)用.

PreparedStmtDB 的類定義如下:

// prepare 模式下的 connPool 實(shí)現(xiàn)類. 
type PreparedStmtDB struct {
    // 各 stmt 實(shí)例. 其中 key 為 sql 模板,stmt 是對封 database/sql 中 *Stmt 的封裝 
    Stmts       map[string]*Stmt
    // ...
    Mux         *sync.RWMutex
    // 內(nèi)置的 ConnPool 字段通常為 database/sql 中的 *DB
    ConnPool
}

Stmt 類是 gorm 框架對 database/sql 標(biāo)準(zhǔn)庫下 Stmt 類的簡單封裝,兩者區(qū)別并不大:

type Stmt struct {
    // database/sql 標(biāo)準(zhǔn)庫下的 statement
    *sql.Stmt
    // 是否處于事務(wù)
    Transaction bool
    // 標(biāo)識當(dāng)前 stmt 是否已初始化完成
    prepared    chan struct{}
    prepareErr  error
}

2.4 執(zhí)行器

接下來介紹的是,gorm 框架執(zhí)行 crud 操作邏輯時(shí)使用到的執(zhí)行器 processor,針對 crud 操作的處理函數(shù)會(huì)以 list 的形式聚合在對應(yīng)類型 processor 的 fns 字段當(dāng)中.

type callbacks struct {
    // 對應(yīng)存儲(chǔ)了 crud 等各類操作對應(yīng)的執(zhí)行器 processor
    // query -> query processor
    // create -> create processor
    // update -> update processor
    // delete -> delete processor
    processors map[string]*processor
}

各類 processor 的初始化是通過 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ù)在請求執(zhí)行過程中,會(huì)根據(jù) crud 的類型,從 callbacks 中獲取對應(yīng)類型的 processor. 比如一筆查詢操作,會(huì)通過 callbacks.Query() 方法獲取對應(yīng)的 processor:

func (cs *callbacks) Query() *processor {
    return cs.processors["query"]
}

執(zhí)行器 processor 具體的類定義如下,其中核心字段包括:

? db:從屬的 gorm.DB 實(shí)例

? Clauses:根據(jù) crud 類型確定的 SQL 格式模板,后續(xù)用于拼接生成 sql

? fns:對應(yīng)于 crud 類型的執(zhí)行函數(shù)鏈

type processor struct {
    // 從屬的 DB 實(shí)例
    db        *DB
    // 拼接 sql 時(shí)的關(guān)鍵字順序. 比如 query 類,固定為 SELECT,FROM,WHERE,GROUP BY, ORDER BY, LIMIT, FOR
    Clauses   []string
    // 對應(yīng)于 crud 類型的執(zhí)行函數(shù)鏈
    fns       []func(*DB)
    callbacks []*callback
}

圖片圖片

所有請求遵循的處理思路都是,首先根據(jù)其從屬的 crud 類型,找到對應(yīng)的 processor,然后調(diào)用 processor 的 Execute 方法,執(zhí)行該 processor 下的 fns 函數(shù)鏈.

這一點(diǎn),在接下來 4、5、 6、 7 章中介紹的 crud 流程都是如此.

// 通用的 processor 執(zhí)行函數(shù),其中對應(yīng)于 crud 的核心操作都被封裝在 processor 對應(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 類型,對 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,獲取對應(yīng)表的 schema 信息
    if stmt.Model != nil {
        // ...
    }


    // 處理 dest 信息,將其添加到 stmt 當(dāng)中
    if stmt.Dest != nil {
        // ...
    }


    // 執(zhí)行一系列的 callback 函數(shù),其中最核心的 create/query/update/delete 操作都被包含在其中了. 還包括了一系列前、后處理函數(shù),具體可見第 3 章
    for _, f := range p.fns {
        f(db)
    }


    //...
    return db
}

在 Execute 方法中,還有一項(xiàng)很重要的事情,是根據(jù) crud 的類型,獲取 sql 拼接格式 clauses,將其賦值到該 processor 的 BuildClauses 字段當(dāng)中. crud 各類 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"}
)

2.5 條件

接下來要介紹的是 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)使用方通過鏈?zhǔn)讲僮骺寺?DB時(shí),對應(yīng)追加的狀態(tài)信息就會(huì)生成一個(gè)新的 clause,追加到 statement 對應(yīng)的 clauses 集合當(dāng)中. 當(dāng)請求實(shí)際執(zhí)行時(shí),會(huì)取出 clauses 集合,拼接生成完整的 sql 用于執(zhí)行.

條件 clause 本身是個(gè)抽象的 interface,定義如下:

// Interface clause interface
type Interface interface {
    // clause 名稱
    Name() string
    // 生成對應(yīng)的 sql 部分
    Build(Builder)
    // 和同類 clause 合并
    MergeClause(*Clause)
}

不同的 clause 有不同的實(shí)現(xiàn)類,我們以 SELECT 為例進(jìn)行展示:

type Select struct {
    // 使用使用 distinct 模式
    Distinct   bool
    // 是否 select 查詢指定的列,如 select id,name
    Columns    []Column
    Expression Expression
}


func (s Select) Name() string {
    return "SELECT"
}


func (s Select) Build(builder Builder) {
    // select  查詢指定的列
    if len(s.Columns) > 0 {
        if s.Distinct {
            builder.WriteString("DISTINCT ")
        }


        // 將指定列追加到 sql 語句中
        for idx, column := range s.Columns {
            if idx > 0 {
                builder.WriteByte(',')
            }
            builder.WriteQuoted(column)
        }
    // 不查詢指定列,則使用 select *
    } else {
        builder.WriteByte('*')
    }
}

拼接 sql 是通過調(diào)用 Statement.Build 方法來實(shí)現(xiàn)的,入?yún)?yīng)的是 crud 中某一類 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 查詢類為例,會(huì)遵循 "SELECT"->"FROM"->"WHERE"->"GROUP BY"->"ORDER BY"->"LIMIT"->"FOR" 的順序,依次從 statement 中獲取對應(yīng)的 clause,通過調(diào)用 clause.Build 方法,將 sql 本文組裝到 statement 的 SQL 字段中.

以 query 流程為例,拼接 sql 的流程入口可以參見 4.3 小節(jié)代碼展示當(dāng)中的 BuildQuerySQL(...) 方法.

3 初始化

圖片圖片

本章中,我們將會(huì)以 gorm.Open 方法作為入口,詳細(xì)展開創(chuàng)建 gorm.DB 實(shí)例的源碼細(xì)節(jié).

3.1 創(chuàng)建 db

gorm.Open 方法是創(chuàng)建 DB 實(shí)例的入口方法,其中包含如下幾項(xiàng)核心步驟:

  • ? 完成 gorm.Config 配置的創(chuàng)建和注入
  • ? 完成連接器 dialector 的注入,本篇使用的是 mysql 版本
  • ? 完成 callbacks 中 crud 等幾類 processor 的創(chuàng)建 ( 通過 initializeCallbacks(...) 方法 )
  • ? 完成 connPool 的創(chuàng)建以及各類 processor fns 函數(shù)的注冊( 通過 dialector.Initialize(...) 方法 )
  • ? 倘若啟用了 prepare 模式,需要使用 preparedStmtDB 進(jìn)行 connPool 的平替
  • ? 構(gòu)造 statement 實(shí)例
  • ? 根據(jù)策略,決定是否通過 ping 請求測試連接
  • ? 返回創(chuàng)建好的 db 實(shí)例
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ì)對 crud 各個(gè)方法的 callback 方法進(jìn)行注冊
        // 會(huì)對 db.connPool 進(jìn)行初始化,通常情況下是 database/sql 庫下 *sql.DB 的類型        
        err = config.Dialector.Initialize(db)
        // ...
    }


    // 是否啟用 prepare 模式
    if config.PrepareStmt {
        preparedStmt := NewPreparedStmtDB(db.ConnPool)
        db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
        // 倘若啟用了 prepare 模式,會(huì)對 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
}

 

3.2 初始化 dialector

mysql 是我們常用的數(shù)據(jù)庫,對應(yīng)于 mysql 版本的 dialector 實(shí)現(xiàn)類位于 github.com/go-sql-driver/mysql 包下. 使用方可以通過 Open 方法,將傳入的 dsn 解析成配置,然后返回 mysql 版本的 Dialector 實(shí)例.

package mysql


func Open(dsn string) gorm.Dialector {
    dsnConf, _ := mysql.ParseDSN(dsn)
    return &Dialector{Config: &Config{DSN: dsn, DSNConfig: dsnConf}}
}

通過 Dialector.Initialize 方法完成連接器初始化操作,其中也會(huì)涉及到對連接池 connPool 的初構(gòu)造,并通過 callbacks.RegisterDefaultCallbacks 方法完成 crud 四類 processor 當(dāng)中 fns 的注冊操作:

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 類操作 callback 函數(shù)的注冊
    callbacks.RegisterDefaultCallbacks(db, callbackConfig)


    // ...
    return
}

3.3 注冊 crud 函數(shù)

圖片圖片

對應(yīng)于 crud 四類 processor,注冊的函數(shù)鏈 fns 的內(nèi)容和順序是固定的,展示如上圖. 相應(yīng)的源碼展示如下,對應(yīng)的方法為 RegisterDefaultCallbacks(...):

func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
    // ...
    //  創(chuàng)建類 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


    // 查詢類 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


    // 刪除類 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


    // 更新類 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 類
    rowCallback := db.Callback().Row()
    rowCallback.Register("gorm:row", RowQuery)
    rowCallback.Clauses = config.QueryClauses


    // raw 類
    rawCallback := db.Callback().Raw()
    rawCallback.Register("gorm:raw", RawExec)
    rawCallback.Clauses = config.QueryClauses
}

注冊某個(gè)特定 fn 函數(shù)的入口是 processor.Register 方法,對應(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
}

4 查詢

圖片圖片

接下來以 db.First 方法作為入口,展示數(shù)據(jù)庫查詢的方法鏈路:

4.1 入口

在 db.First 方法當(dāng)中:

? 遵循 First 的語義,通過 limit 和 order 追加 clause,限制只取滿足條件且主鍵最小的一筆數(shù)據(jù)

? 追加用戶傳入的一系列 condition,進(jìn)行 clause 追加

? 在 First、Take、Last 等方法中,會(huì)設(shè)置 RaiseErrorOnNotFound 標(biāo)識為 true,倘若未找到記錄,則會(huì)拋出 ErrRecordNotFound 錯(cuò)誤

var ErrRecordNotFound = logger.ErrRecordNotFound

? 設(shè)置 statement 中的 dest 為用戶傳入的 dest,作為反序列化響應(yīng)結(jié)果的對象實(shí)例

? 獲取 query 類型的 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)
}

 

4.2 添加條件

圖片圖片

執(zhí)行查詢類操作時(shí),通常會(huì)通過鏈?zhǔn)秸{(diào)用的方式,傳入一些查詢限制條件,比如 Where、Group By、Order、Limit 之類. 我們以 Limit 為例,進(jìn)行展開介紹:

  • ? 首先調(diào)用 db.getInstance() 方法,克隆出一份 DB 會(huì)話實(shí)例
  • ? 調(diào)用 statement.AddClause 方法,將 limit 條件追加到 statement 的 Clauses map 中
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   
}

 

4.3 核心方法

圖片圖片

在 query 類型 processor 的 fns 函數(shù)鏈中,最主要的函數(shù)是 Query,其中涉及的核心步驟包括:

? 調(diào)用 BuildQuerySQL(...) 方法,根據(jù)傳入的 clauses 組裝生成 sql

? 調(diào)用 connPool.QueryContext(...) ,完成查詢類 sql 的執(zhí)行,返回查到的行數(shù)據(jù) rows(非 prepare 模式下,此處會(huì)對接 database/sql 庫,走到 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)
        }
    }
}

 

4.4 掃描數(shù)據(jù)

接下來展示一下,gorm.Scan() 方法,其作用是將查詢結(jié)果數(shù)據(jù)反序列化到 dest 當(dāng)中:

  • ? 通過對 statement 中的 dest 進(jìn)行分類,采取的不同的處理方式
  • ? 核心方法都是通過 rows.Scan(...) 方法,將響應(yīng)數(shù)據(jù)反序列化到 dest 當(dāng)中
  • ? 調(diào)用 rows.Err() 方法,拋出請求過程中遇到的錯(cuò)誤
  • ? 倘若啟用了 RaiseErrorOnNotFound 模式且查詢到的行數(shù)為 0,則拋出錯(cuò)誤 ErrRecordNotFound

對應(yīng)源碼展示如下:

// Scan 方法將 rows 中的數(shù)據(jù)掃描解析到 db statement 中的 dest 當(dāng)中
// 其中 rows 通常為 database/sql 下的 *Rows 類型
// 掃描數(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 類型進(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 類型進(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)識為 true,在沒有查找到數(shù)據(jù)時(shí),會(huì)拋出 ErrRecordNotFound 錯(cuò)誤
    if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound && db.Error == nil {
        db.AddError(ErrRecordNotFound)
    }
}

5 創(chuàng)建

圖片圖片

本章以 db.Create(...) 方法為入口,展開介紹一下創(chuàng)建數(shù)據(jù)記錄的流程.

5.1 入口

創(chuàng)建數(shù)據(jù)記錄操作主要通過調(diào)用 gorm.DB 的 Create 方法完成,其包括如下核心步驟:

  • ? 通過 db.getInstance() 克隆出一個(gè) DB 會(huì)話實(shí)例
  • ? 設(shè)置 statement 中的 dest 為用戶傳入的 dest
  • ? 獲取到 create 類型的 processor
  • ? 調(diào)用 processor 的 Execute 方法,遍歷執(zhí)行 fns 函數(shù)鏈,完成創(chuàng)建操作
// Create inserts value, returning the inserted data's primary key in value's id
func (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)
}

 

5.2 核心方法

在 create 類型 processor 的 fns 函數(shù)鏈中,最主要的執(zhí)行函數(shù)就是 Create,其中核心步驟包括:

? 調(diào)用 statement.Build(...) 方法,生成 sql

? 調(diào)用 connPool.ExecContext(...) 方法,請求 mysql 服務(wù)端執(zhí)行 sql(默認(rèn)情況下,此處會(huì)使用 database/sql 標(biāo)準(zhǔn)庫的 db.ExecContext(...) 方法)

? 調(diào)用 result.RowsAffected() ,獲取到本次創(chuàng)建操作影響的數(shù)據(jù)行數(shù)

// Create create hook
func 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()        
        // ...
    }
}

6 刪除

圖片圖片

接下來是數(shù)據(jù)記錄刪除流程,以 db.Delete 方法作為走讀入口:

6.1 入口

在 db.Delete 方法中,核心步驟包括:

  • ? 通過 db.getInstance() 方法獲取 db 的克隆實(shí)例
  • ? 通過 statement.AddClause(...) 方法追加使用方傳入的條件 condition
  • ? 設(shè)置 statement dest 為使用方傳入的 value
  • ? 獲取 delete 類型的 processor
  • ? 執(zhí)行 processor.Execute(...) 方法,遍歷調(diào)用 fns 函數(shù)鏈
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)
}

6.2 核心方法

在 delete 類型的 processor 的 fns 函數(shù)鏈中,最核心的函數(shù)是 Delete,其中的核心步驟包括:

  • ? 調(diào)用 statement.Build(...) 方法,生成 sql
  • ? 倘若未啟用 AllowGlobalUpdate 模式,則會(huì)校驗(yàn)使用方是否設(shè)置了 where 條件,未設(shè)置會(huì)拋出 gorm.ErrMissingWhereClause 錯(cuò)誤(對應(yīng) checkMissingWhereConditions() 方法)
var ErrMissingWhereClause = errors.New("WHERE conditions required")
  • ? 調(diào)用 connPool.ExecContext(...) 方法,執(zhí)行刪除操作(默認(rèn)使用的是標(biāo)準(zhǔn)庫 database/sql 中的 db.ExecContxt(...) 方法)
  • ? 調(diào)用 result.RowsAffected() 方法,獲取本次刪除操作影響的數(shù)據(jù)行數(shù)
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)識不為 true 且 error 為空,則需要對 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
    }
}

7 更新

圖片圖片

下面展示的是通過 gorm 更新數(shù)據(jù)的流程,以 db.Update(...) 方法作為源碼走讀的入口:

7.1 入口

在 db.Update 方法中,核心步驟包括:

? 通過 db.getInstance() 方法獲取 db 的克隆實(shí)例

? 設(shè)置 statement dest 為使用方傳入的 value

? 獲取 update 類型的 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)
}

7.2 核心方法

在 update 類型 processor 的 fns 函數(shù)鏈中,最核心的函數(shù)就是 Update,其中核心步驟包括:

? 調(diào)用 statement.Build(...) 方法,生成 sql

? 和 Delete 流程類似,倘若未啟用 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)庫的 db.ExecContext(...) 方法)

? 調(diào)用 result.RowsAffected() 方法,獲取到本次更新操作影響的行數(shù)

// Update update hook
func 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()
            }       
        }
    }
}

 

8 事務(wù)

圖片圖片

通過 gorm 框架同樣能夠很方便地使用事務(wù)相關(guān)的功能:

? 調(diào)用 db.Transaction(...) 方法

? 傳入閉包函數(shù) fc,其中入?yún)?tx 為帶有事務(wù)會(huì)話屬性的 db 實(shí)例,后續(xù)事務(wù)內(nèi)所有執(zhí)行操作都需要圍繞這個(gè) tx 展開

? 可以使用該 tx 實(shí)例完成事務(wù)的提交 tx.Commit() 和回滾 tx.Rollback() 操作

8.1 入口

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)為用戶執(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 {
        // 開啟事務(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
}

 

8.2 開啟事務(wù)

對于 DB.Begin() 方法,在默認(rèn)模式下會(huì)使用 database/sql 庫下的 sql.DB.BeginTx 方法創(chuàng)建出一個(gè) sql.Tx 對象,將其賦給當(dāng)前事務(wù)會(huì)話 DB 的 statement.ConnPool 字段,以供后續(xù)使用:

// Begin begins a transaction with any transaction options opts
func (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
}

 

8.3 提交&回滾

事務(wù)的提交和回滾操作,會(huì)執(zhí)行 statement 中的 connPool 的 Commit 和 Rollback 方法完成:

? 執(zhí)行事務(wù)提交操作:

// Commit commits the changes in a transaction
func (db *DB) Commit() *DB {
    // 默認(rèn)情況下,此處的 ConnPool 實(shí)現(xiàn)類為 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 transaction
func (db *DB) Rollback() *DB {
    // 默認(rèn)情況下,此處的 ConnPool 實(shí)現(xiàn)類為 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
}

 

9 預(yù)處理

圖片圖片

倘若創(chuàng)建 gorm.DB 時(shí),倘若在 Config 中設(shè)置了 PrepareStmt 標(biāo)識,則代表后續(xù)會(huì)啟用 prepare 預(yù)處理模式. 次吃,在執(zhí)行 query 或者 exec 操作時(shí),使用的 ConnPool 的實(shí)現(xiàn)版本是 PreparedStmtDB,執(zhí)行時(shí)會(huì)拆分為兩個(gè)步驟:

? 通過 PreparedStmtDB.prepare(...) 操作創(chuàng)建/復(fù)用 stmt,后續(xù)相同 sql 模板可以復(fù)用此 stmt

? 通過 stmt.Query(...)/Exec(...) 執(zhí)行 sql

9.1 prepare

在 PreparedStmtDB.prepare 方法中,會(huì)通過加鎖 double check 的方式,創(chuàng)建或復(fù)用 sql 模板對應(yīng)的 stmt. 創(chuàng)建 stmt 的操作通過調(diào)用 conn.PrepareContext 方法完成.(通常此處的 conn 為 database/sql 庫下的 sql.DB)

PreparedStmtDB.prepare 方法核心流程梳理如下:

? 加讀鎖,然后以 sql 模板為 key,嘗試從 db.Stmts map 中獲取 stmt 復(fù)用

? 倘若 stmt 不存在,則加寫鎖 double check

? 調(diào)用 conn.PrepareContext(...) 方法,創(chuàng)建新的 stmt,并存放到 map 中供后續(xù)復(fù)用

完整的代碼和對應(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ā)場景下,只允許有一個(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)檫€通過 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
}

 

9.2 查詢

在 prepare 模式下,查詢操作通過 PreparedStmtDB.QueryContext(...) 方法實(shí)現(xiàn). 首先通過 PreparedStmtDB.prepare(...) 方法嘗試復(fù)用 stmt,然后調(diào)用 stmt.QueryContext(...) 執(zhí)行查詢操作.

此處 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
}

 

9.3 執(zhí)行

在 prepare 模式下,執(zhí)行操作通過 PreparedStmtDB.ExecContext(...) 方法實(shí)現(xiàn). 首先通過 PreparedStmtDB.prepare(...) 方法嘗試復(fù)用 stmt,然后調(diào)用 stmt.ExecContext(...) 執(zhí)行查詢操作.

此處 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
}

 

10 總結(jié)

本期主要和大家一起解析了 gorm 框架的底層實(shí)現(xiàn)原理.

通篇學(xué)習(xí)下來,相信大家也能夠看出,gorm 框架名副其實(shí),正是基于 orm 的思想,為使用方屏蔽了大量和 sql、db 有關(guān)的細(xì)節(jié),讓使用方能夠像操作對象一樣完成和數(shù)據(jù)庫的交互操作.

責(zé)任編輯:武曉燕 來源: 小徐先生的編程世界
相關(guān)推薦

2012-11-06 11:07:59

jQueryJSjQuery框架

2016-12-15 09:44:31

框架Caffe源碼

2021-10-27 16:52:37

LayoutInfl源碼解析

2025-02-06 08:24:25

AQS開發(fā)Java

2023-11-17 12:11:26

GORMGo Web

2015-09-11 09:17:55

JavaJava HashMa

2021-09-16 15:08:08

鴻蒙HarmonyOS應(yīng)用

2021-09-09 06:55:43

AndroidViewDragHel原理

2022-12-09 08:10:12

kubectl容器源碼

2024-09-04 11:42:17

Vue3.5源碼API

2009-10-27 11:16:20

VB.NET應(yīng)用框架

2020-10-10 08:20:27

Spring Boot運(yùn)行原理代碼

2022-07-19 20:04:31

NAPI模塊鴻蒙

2013-06-08 10:11:31

Java線程池架構(gòu)

2015-10-10 09:39:42

Java線程池源碼解析

2021-08-27 07:47:07

Nacos灰度源碼

2022-05-12 13:50:13

OOPTCC中間件

2024-11-08 13:04:08

項(xiàng)目Hertz接口

2014-12-11 13:37:13

WPF架構(gòu)

2017-11-28 15:36:25

換扶技術(shù)Android資源
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號