日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

Gorm 框架原理&源碼解析

來源: 責編: 時間:2024-01-18 09:39:10 221觀看
導讀0 前言本篇將和大家探討 go 語言中最流行的 orm 框架 ——gorm 的底層實現原理.本篇分享內容的目錄大綱如下所示:圖片1 入口圖片gorm 框架是國內的大神 jinzhu 基于 go 語言開源實現的一款數據庫 orm 框架. 【gorm】一

0 前言

本篇將和大家探討 go 語言中最流行的 orm 框架 ——gorm 的底層實現原理.tw228資訊網——每日最新資訊28at.com

本篇分享內容的目錄大綱如下所示:tw228資訊網——每日最新資訊28at.com

圖片圖片tw228資訊網——每日最新資訊28at.com

1 入口

圖片圖片tw228資訊網——每日最新資訊28at.com

gorm 框架是國內的大神 jinzhu 基于 go 語言開源實現的一款數據庫 orm 框架. 【gorm】一詞恢弘大氣,前綴 go 代表 go 語言, 后綴 orm 全稱 Object Relation Mapping,指的是使用對象映射的方式,讓使用方能夠像操作本地對象實例一樣輕松便捷地完成遠端數據庫的操作.tw228資訊網——每日最新資訊28at.com

gorm 框架開源地址為: https://github.com/go-gorm/gormtw228資訊網——每日最新資訊28at.com

本期會涉及到大量 gorm 的源碼走讀環節,使用的代碼版本為 tag: v.1.25.5tw228資訊網——每日最新資訊28at.com

1.1 初始化

gorm 框架通過一個 gorm.DB 實例來指代我們所操作的數據庫. 使用 gorm 的第一步就是要通過 Open 方法創建出一個 gorm.DB 實例,其中首個入參為連接器 dialector,本身是個抽象的 interface,其實現類關聯了具體數據庫類型.tw228資訊網——每日最新資訊28at.com

本文將統一以 mysql 為例,注入 gorm.io/driver/mysql 包下定義的 dialector 類.tw228資訊網——每日最新資訊28at.com

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(){       // 創建 db 實例       db, err = gorm.Open(mysql.Open(dsn),&gorm.Config{})    })      return db,err}

本文將以 gorm.Open 方法為入口,在第 3 章中深入源碼底層鏈路.tw228資訊網——每日最新資訊28at.com

1.2 po 模型

基于 orm 的思路,與某張數據表所關聯映射的是 po (persist object)模型.tw228資訊網——每日最新資訊28at.com

定義 po 類時,可以通過聲明 TableName 方法,來指定該類對應的表名.tw228資訊網——每日最新資訊28at.com

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

 tw228資訊網——每日最新資訊28at.com

定義 po 類時,可以通過組合 gorm.Model 的方式,完成主鍵、增刪改時間等4列信息的一鍵添加,并且由于聲明了 DeletedAt 字段,gorm 將會默認會啟動軟刪除模式. (有關軟刪除的內容,可參見前文——gorm 框架使用教程)tw228資訊網——每日最新資訊28at.com

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

1.3 查詢

下面展示的是使用 gorm 進行數據查詢操作的代碼示例. 本文第 4 章會以 db.First(...) 方法為入口,展開底層源碼鏈路的走讀.tw228資訊網——每日最新資訊28at.com

func Test_query(t *testing.T) {    // 獲取 db     db, _ := getDB()    // 超時控制    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 創建

下面展示的是使用 gorm 進行數據創建操作的代碼示例. 本文第 5 章會以 db.Create(...) 方法為入口,展開底層源碼鏈路的走讀.tw228資訊網——每日最新資訊28at.com

func Test_create(t *testing.T) {    // 獲取 db 實例    db, _ := getDB()        // 構造 po 實例    r := Reward{        Amount: sql.NullInt64{            Int64: 0,            Valid: true,        },        Type:   "money",        UserID: 123,    }    // 超時控制    ctx, cancel := context.WithTimeout(context.Background(), time.Second)    defer cancel()    // 創建    if err := db.WithContext(ctx).Create(&r).Error; err != nil {        t.Error(err)        return    }}

1.5 刪除

下面展示的是使用 gorm 進行數據刪除操作的代碼示例. 本文第 6 章會以 db.Delete(...) 方法為入口,展開底層源碼鏈路的走讀.tw228資訊網——每日最新資訊28at.com

func Test_delete(t *testing.T) {    // 獲取 db 實例    db, _ := getDB()        // 構造 po 實例    r := Reward{      }    // 超時控制    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 進行數據更新操作的代碼示例. 本文第 7 章會以 db.Update(...) 方法為入口,展開底層源碼鏈路的走讀.tw228資訊網——每日最新資訊28at.com

func Test_update(t *testing.T) {    // 獲取 db 實例    db, _ := getDB()        // 構造 po 實例    r := Reward{        Amount: sql.NullInt64{            Int64: 1000,            Valid: true,        }             }    // 超時控制    ctx, cancel := context.WithTimeout(context.Background(), time.Second)    defer cancel()    // 更新主鍵 id 為 2 的記錄,將金額設置為 1000    if err := db.WithContext(ctx).Where("id = ?",2).Update(&r).Error; err != nil {        t.Error(err)        return    }}

1.7 事務

下面展示的是使用 gorm 開啟事務的代碼示例. 本文第 8 章會以 db.Transaction(...) 方法為入口,展開底層源碼鏈路的走讀.tw228資訊網——每日最新資訊28at.com

func Test_tx(t *testing.T) {    // 獲取 db 實例    db, _ := getDB()    // 事務內的執行邏輯    do := func(ctx context.Context, tx *gorm.DB) error {        // do somethine ...        return nil    }    // 超時控制    ctx, cancel := context.WithTimeout(context.Background(), time.Second)    defer cancel()    // 執行事務    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    }}

 tw228資訊網——每日最新資訊28at.com

2 核心類

本章中,我會首先向大家介紹 gorm 框架中各個核心類的定義.tw228資訊網——每日最新資訊28at.com

2.1 數據庫

gorm.DB 是 gorm 定義的數據庫類. 所有執行的數據庫的操作都將緊密圍繞這個類,以鏈式調用的方式展開. 每當執行過鏈式調用后,新生成的 DB 對象中就存儲了一些當前請求特有的狀態信息,我們把這種對象稱作“會話”.tw228資訊網——每日最新資訊28at.com

DB 類中的核心字段包括:tw228資訊網——每日最新資訊28at.com

? Config:用戶自定義的配置項tw228資訊網——每日最新資訊28at.com

? Error:一次會話執行過程中遇到的錯誤tw228資訊網——每日最新資訊28at.com

? RowsAffected:該請求影響的行數tw228資訊網——每日最新資訊28at.com

? Statement:一次會話的狀態信息,比如請求和響應信息tw228資訊網——每日最新資訊28at.com

? clone:會話被克隆的次數. 倘若 clone = 1,代表是始祖 DB 實例;倘若 clone > 1,代表是從始祖 DB 克隆出來的會話tw228資訊網——每日最新資訊28at.com

DB 類定義的代碼如下:tw228資訊網——每日最新資訊28at.com

// gorm 中定義的數據庫類// 所有 orm 的思想type DB struct {    // 配置    *Config    // 錯誤    Error        error    // 影響的行數    RowsAffected int64    // 會話狀態信息    Statement    *Statement    // 克隆次數    clone        int}

I 錯誤處理

DB 類的 AddError 方法,用于在會話執行過程中拋出錯誤.tw228資訊網——每日最新資訊28at.com

一次會話在執行過程中可能會遇到多個錯誤,因此會通過 error wrapping 的方式,實現錯誤的拼接.tw228資訊網——每日最新資訊28at.com

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 表名設置

圖片圖片tw228資訊網——每日最新資訊28at.com

請求在執行時,需要明確操作的是哪張數據表.tw228資訊網——每日最新資訊28at.com

使用方可以通過鏈式調用 DB.Table 方法,顯式聲明本次操作所針對的數據表,這種方式的優先級是最高的.tw228資訊網——每日最新資訊28at.com

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

在 DB.Table 方法缺省的情況下,gorm 則會嘗試通過 po 類的 TableName 方法獲取表名.tw228資訊網——每日最新資訊28at.com

在 gorm 中聲明了一個 tabler interface:tw228資訊網——每日最新資訊28at.com

type Tabler interface {    TableName() string}

倘若 po 模型聲明了 TableName 方法,則隱式實現了該 interface,在處理過程中會被斷言成 tabler 類型,然后調用 TableName 方法獲取其表名.tw228資訊網——每日最新資訊28at.com

該流程對應的源碼展示如下:tw228資訊網——每日最新資訊28at.com

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,然后調用 TableName 方法獲取表名    if tabler, ok := modelValue.Interface().(Tabler); ok {        tableName = tabler.TableName()    }    // ...    // 將表名信息添加到 schema 當中    schema := &Schema{        // ...        Table:            tableName,        // ...    }    // ...    return schema, schema.err}

2.2 會話狀態

接下來介紹的是 gorm 中非常核心的 statement 類,里面存儲了一次會話中包含的狀態信息,比如請求中的條件、sql 語句拼接格式、響應參數類型、數據表的名稱等等.tw228資訊網——每日最新資訊28at.com

statement 類中涉及到的各個核心字段通過下方的代碼和注釋加以介紹:tw228資訊網——每日最新資訊28at.com

// Statement statementtype Statement struct {    // 數據庫實例    *DB    // ...    // 表名    Table                string    // 操作的 po 模型    Model                interface{}    // ...    // 處理結果反序列化到此處    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    // 在未查找到數據記錄時,是否拋出 recordNotFound 錯誤    RaiseErrorOnNotFound bool    // ...    // 執行的 sql,調用 state.Build 方法后,會將 sql 各部分文本依次追加到其中. 具體可見 2.5 小節    SQL                  strings.Builder    // 存儲的變量    Vars                 []interface{}    // ...}

I connPool

這里額外強調一下 connPool 字段,其含義是連接池,和數據庫的交互操作都需要依賴它才得以執行. connPool 本身是個 interface,定義如下:tw228資訊網——每日最新資訊28at.com

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 根據是否啟用了 prepare 預處理模式,存在不同的實現類版本:tw228資訊網——每日最新資訊28at.com

  • ? **在普通模式下,connPool 的實現類為 database/sql 庫下的 *DB 類**(詳細內容參見前文——Golang sql 標準庫源碼解析)
  • ? 在 prepare 模式下,connPool 實現類型為 gorm 中定義的 PreparedStmtDB 類,在本文 2.3 小節中展開

II db 克隆

此處額外介紹一下 DB 的克隆流程,所有在始祖 DB 基礎上追加狀態信息,克隆出來的 DB 實例都可以稱為“會話”.tw228資訊網——每日最新資訊28at.com

會話的狀態信息主要存儲在 statement 當中的,所以在克隆 DB 時,很重要的一環就是完成對 其中 statement 部分的創建/克隆.tw228資訊網——每日最新資訊28at.com

該流程對應的方法為 DB.getInstance 方法,主要通過 DB 中的 clone 字段來判斷當前是首次從始祖 DB 中執行克隆操作還是在一個會話的基礎上克隆出一個新的會話實例,對應的源碼展示如下:tw228資訊網——每日最新資訊28at.com

func (db *DB) getInstance() *DB {    if db.clone > 0 {        tx := &DB{Config: db.Config, Error: db.Error}        // 倘若是首次對 db 進行 clone,則需要構造出一個新的 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),            }        // 倘若已經 db clone 過了,則還需要 clone 原先的 statement        } else {            // with clone statement            tx.Statement = db.Statement.clone()            tx.Statement.DB = tx        }        return tx    }    return db}

 tw228資訊網——每日最新資訊28at.com

2.3 預處理DB

圖片圖片tw228資訊網——每日最新資訊28at.com

在 prepare 預處理模式下,DB 中連接池 connPool 的實現類為 PreparedStmtDB. 定義該類的目的是為了使用 database/sql 標準庫中的 prepare 能力,完成預處理狀態 statement 的構造和復用.tw228資訊網——每日最新資訊28at.com

PreparedStmtDB 的類定義如下:tw228資訊網——每日最新資訊28at.com

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

Stmt 類是 gorm 框架對 database/sql 標準庫下 Stmt 類的簡單封裝,兩者區別并不大:tw228資訊網——每日最新資訊28at.com

type Stmt struct {    // database/sql 標準庫下的 statement    *sql.Stmt    // 是否處于事務    Transaction bool    // 標識當前 stmt 是否已初始化完成    prepared    chan struct{}    prepareErr  error}

2.4 執行器

接下來介紹的是,gorm 框架執行 crud 操作邏輯時使用到的執行器 processor,針對 crud 操作的處理函數會以 list 的形式聚合在對應類型 processor 的 fns 字段當中.tw228資訊網——每日最新資訊28at.com

type callbacks struct {    // 對應存儲了 crud 等各類操作對應的執行器 processor    // query -> query processor    // create -> create processor    // update -> update processor    // delete -> delete processor    processors map[string]*processor}

各類 processor 的初始化是通過 initializeCallbacks 方法完成,該方法的調用入口在本文 3.1 小節的 gorm.Open 方法中.tw228資訊網——每日最新資訊28at.com

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

后續在請求執行過程中,會根據 crud 的類型,從 callbacks 中獲取對應類型的 processor. 比如一筆查詢操作,會通過 callbacks.Query() 方法獲取對應的 processor:tw228資訊網——每日最新資訊28at.com

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

執行器 processor 具體的類定義如下,其中核心字段包括:tw228資訊網——每日最新資訊28at.com

? db:從屬的 gorm.DB 實例tw228資訊網——每日最新資訊28at.com

? Clauses:根據 crud 類型確定的 SQL 格式模板,后續用于拼接生成 sqltw228資訊網——每日最新資訊28at.com

? fns:對應于 crud 類型的執行函數鏈tw228資訊網——每日最新資訊28at.com

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

圖片圖片tw228資訊網——每日最新資訊28at.com

所有請求遵循的處理思路都是,首先根據其從屬的 crud 類型,找到對應的 processor,然后調用 processor 的 Execute 方法,執行該 processor 下的 fns 函數鏈.tw228資訊網——每日最新資訊28at.com

這一點,在接下來 4、5、 6、 7 章中介紹的 crud 流程都是如此.tw228資訊網——每日最新資訊28at.com

// 通用的 processor 執行函數,其中對應于 crud 的核心操作都被封裝在 processor 對應的 fns list 當中了func (p *processor) Execute(db *DB) *DB {    // call scopes    var (        // ...        stmt = db.Statement        // ...    )    if len(stmt.BuildClauses) == 0 {        // 根據 crud 類型,對 buildClauses 進行復制,用于后續的 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,獲取對應表的 schema 信息    if stmt.Model != nil {        // ...    }    // 處理 dest 信息,將其添加到 stmt 當中    if stmt.Dest != nil {        // ...    }    // 執行一系列的 callback 函數,其中最核心的 create/query/update/delete 操作都被包含在其中了. 還包括了一系列前、后處理函數,具體可見第 3 章    for _, f := range p.fns {        f(db)    }    //...    return db}

在 Execute 方法中,還有一項很重要的事情,是根據 crud 的類型,獲取 sql 拼接格式 clauses,將其賦值到該 processor 的 BuildClauses 字段當中. crud 各類 clauses 格式展示如下:tw228資訊網——每日最新資訊28at.com

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. 一條執行 sql 中,各個部分都屬于一個 clause,比如一條 SELECT * FROM reward WHERE id < 10 ORDER by id 的 SQL,其中就包含了 SELECT、FROM、WHERE 和 ORDER 四個 clause.tw228資訊網——每日最新資訊28at.com

當使用方通過鏈式操作克隆 DB時,對應追加的狀態信息就會生成一個新的 clause,追加到 statement 對應的 clauses 集合當中. 當請求實際執行時,會取出 clauses 集合,拼接生成完整的 sql 用于執行.tw228資訊網——每日最新資訊28at.com

條件 clause 本身是個抽象的 interface,定義如下:tw228資訊網——每日最新資訊28at.com

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

不同的 clause 有不同的實現類,我們以 SELECT 為例進行展示:tw228資訊網——每日最新資訊28at.com

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 是通過調用 Statement.Build 方法來實現的,入參對應的是 crud 中某一類 processor 的 BuildClauses.tw228資訊網——每日最新資訊28at.com

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 查詢類為例,會遵循 "SELECT"->"FROM"->"WHERE"->"GROUP BY"->"ORDER BY"->"LIMIT"->"FOR" 的順序,依次從 statement 中獲取對應的 clause,通過調用 clause.Build 方法,將 sql 本文組裝到 statement 的 SQL 字段中.tw228資訊網——每日最新資訊28at.com

以 query 流程為例,拼接 sql 的流程入口可以參見 4.3 小節代碼展示當中的 BuildQuerySQL(...) 方法.tw228資訊網——每日最新資訊28at.com

3 初始化

圖片圖片tw228資訊網——每日最新資訊28at.com

本章中,我們將會以 gorm.Open 方法作為入口,詳細展開創建 gorm.DB 實例的源碼細節.tw228資訊網——每日最新資訊28at.com

3.1 創建 db

gorm.Open 方法是創建 DB 實例的入口方法,其中包含如下幾項核心步驟:tw228資訊網——每日最新資訊28at.com

  • ? 完成 gorm.Config 配置的創建和注入
  • ? 完成連接器 dialector 的注入,本篇使用的是 mysql 版本
  • ? 完成 callbacks 中 crud 等幾類 processor 的創建 ( 通過 initializeCallbacks(...) 方法 )
  • ? 完成 connPool 的創建以及各類 processor fns 函數的注冊( 通過 dialector.Initialize(...) 方法 )
  • ? 倘若啟用了 prepare 模式,需要使用 preparedStmtDB 進行 connPool 的平替
  • ? 構造 statement 實例
  • ? 根據策略,決定是否通過 ping 請求測試連接
  • ? 返回創建好的 db 實例
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 當中的各個 processor    db.callbacks = initializeCallbacks(db)    // ...    if config.Dialector != nil {        // 在其中會對 crud 各個方法的 callback 方法進行注冊        // 會對 db.connPool 進行初始化,通常情況下是 database/sql 庫下 *sql.DB 的類型                err = config.Dialector.Initialize(db)        // ...    }    // 是否啟用 prepare 模式    if config.PrepareStmt {        preparedStmt := NewPreparedStmtDB(db.ConnPool)        db.cacheStore.Store(preparedStmtDBKey, preparedStmt)        // 倘若啟用了 prepare 模式,會對 conn 進行替換        db.ConnPool = preparedStmt    }    // 構造一個 statement 用于存儲處理鏈路中的一些狀態信息    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}

 tw228資訊網——每日最新資訊28at.com

3.2 初始化 dialector

mysql 是我們常用的數據庫,對應于 mysql 版本的 dialector 實現類位于 github.com/go-sql-driver/mysql 包下. 使用方可以通過 Open 方法,將傳入的 dsn 解析成配置,然后返回 mysql 版本的 Dialector 實例.tw228資訊網——每日最新資訊28at.com

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

通過 Dialector.Initialize 方法完成連接器初始化操作,其中也會涉及到對連接池 connPool 的初構造,并通過 callbacks.RegisterDefaultCallbacks 方法完成 crud 四類 processor 當中 fns 的注冊操作:tw228資訊網——每日最新資訊28at.com

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 函數的注冊    callbacks.RegisterDefaultCallbacks(db, callbackConfig)    // ...    return}

3.3 注冊 crud 函數

圖片圖片tw228資訊網——每日最新資訊28at.com

對應于 crud 四類 processor,注冊的函數鏈 fns 的內容和順序是固定的,展示如上圖. 相應的源碼展示如下,對應的方法為 RegisterDefaultCallbacks(...):tw228資訊網——每日最新資訊28at.com

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

注冊某個特定 fn 函數的入口是 processor.Register 方法,對應的核心源碼鏈路展示如下:tw228資訊網——每日最新資訊28at.com

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

圖片圖片tw228資訊網——每日最新資訊28at.com

接下來以 db.First 方法作為入口,展示數據庫查詢的方法鏈路:tw228資訊網——每日最新資訊28at.com

4.1 入口

在 db.First 方法當中:tw228資訊網——每日最新資訊28at.com

? 遵循 First 的語義,通過 limit 和 order 追加 clause,限制只取滿足條件且主鍵最小的一筆數據tw228資訊網——每日最新資訊28at.com

? 追加用戶傳入的一系列 condition,進行 clause 追加tw228資訊網——每日最新資訊28at.com

? 在 First、Take、Last 等方法中,會設置 RaiseErrorOnNotFound 標識為 true,倘若未找到記錄,則會拋出 ErrRecordNotFound 錯誤tw228資訊網——每日最新資訊28at.com

var ErrRecordNotFound = logger.ErrRecordNotFound

? 設置 statement 中的 dest 為用戶傳入的 dest,作為反序列化響應結果的對象實例tw228資訊網——每日最新資訊28at.com

? 獲取 query 類型的 processor,調用 Execute 方法執行其中的 fn 函數鏈,完成 query 操作tw228資訊網——每日最新資訊28at.com

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

 tw228資訊網——每日最新資訊28at.com

4.2 添加條件

圖片圖片tw228資訊網——每日最新資訊28at.com

執行查詢類操作時,通常會通過鏈式調用的方式,傳入一些查詢限制條件,比如 Where、Group By、Order、Limit 之類. 我們以 Limit 為例,進行展開介紹:tw228資訊網——每日最新資訊28at.com

  • ? 首先調用 db.getInstance() 方法,克隆出一份 DB 會話實例
  • ? 調用 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   }

 tw228資訊網——每日最新資訊28at.com

4.3 核心方法

圖片圖片tw228資訊網——每日最新資訊28at.com

在 query 類型 processor 的 fns 函數鏈中,最主要的函數是 Query,其中涉及的核心步驟包括:tw228資訊網——每日最新資訊28at.com

? 調用 BuildQuerySQL(...) 方法,根據傳入的 clauses 組裝生成 sqltw228資訊網——每日最新資訊28at.com

? 調用 connPool.QueryContext(...) ,完成查詢類 sql 的執行,返回查到的行數據 rows(非 prepare 模式下,此處會對接 database/sql 庫,走到 sql.DB.QueryContext(...) 方法中)tw228資訊網——每日最新資訊28at.com

? 調用 gorm.Scan() 方法,將結果數據反序列化到 statement 的 dest 當中tw228資訊網——每日最新資訊28at.com

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

 tw228資訊網——每日最新資訊28at.com

4.4 掃描數據

接下來展示一下,gorm.Scan() 方法,其作用是將查詢結果數據反序列化到 dest 當中:tw228資訊網——每日最新資訊28at.com

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

對應源碼展示如下:tw228資訊網——每日最新資訊28at.com

// Scan 方法將 rows 中的數據掃描解析到 db statement 中的 dest 當中// 其中 rows 通常為 database/sql 下的 *Rows 類型// 掃描數據的核心在于調用了 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    )    // 影響的行數    db.RowsAffected = 0    // 根據 dest 類型進行斷言分配    switch dest := db.Statement.Dest.(type) {            case map[string]interface{}, *map[string]interface{}:        if initialized || rows.Next() {            // ...            db.RowsAffected++            // 掃描數據的核心在于,調用 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:        // ...        // 根據 dest 類型進行前處理 ...        db.AddError(rows.Scan(dest))        // ...    }    // 倘若 rows 中存在錯誤,需要拋出    if err := rows.Err(); err != nil && err != db.Error {        db.AddError(err)    }    // 在 first、last、take 模式下,RaiseErrorOnNotFound 標識為 true,在沒有查找到數據時,會拋出 ErrRecordNotFound 錯誤    if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound && db.Error == nil {        db.AddError(ErrRecordNotFound)    }}

5 創建

圖片圖片tw228資訊網——每日最新資訊28at.com

本章以 db.Create(...) 方法為入口,展開介紹一下創建數據記錄的流程.tw228資訊網——每日最新資訊28at.com

5.1 入口

創建數據記錄操作主要通過調用 gorm.DB 的 Create 方法完成,其包括如下核心步驟:tw228資訊網——每日最新資訊28at.com

  • ? 通過 db.getInstance() 克隆出一個 DB 會話實例
  • ? 設置 statement 中的 dest 為用戶傳入的 dest
  • ? 獲取到 create 類型的 processor
  • ? 調用 processor 的 Execute 方法,遍歷執行 fns 函數鏈,完成創建操作
// Create inserts value, returning the inserted data's primary key in value's idfunc (db *DB) Create(value interface{}) (tx *DB) {    // ...    // 克隆 db 會話實例    tx = db.getInstance()    // 設置 dest    tx.Statement.Dest = value    // 執行 create processor    return tx.callbacks.Create().Execute(tx)}

 tw228資訊網——每日最新資訊28at.com

5.2 核心方法

在 create 類型 processor 的 fns 函數鏈中,最主要的執行函數就是 Create,其中核心步驟包括:tw228資訊網——每日最新資訊28at.com

? 調用 statement.Build(...) 方法,生成 sqltw228資訊網——每日最新資訊28at.com

? 調用 connPool.ExecContext(...) 方法,請求 mysql 服務端執行 sql(默認情況下,此處會使用 database/sql 標準庫的 db.ExecContext(...) 方法)tw228資訊網——每日最新資訊28at.com

? 調用 result.RowsAffected() ,獲取到本次創建操作影響的數據行數tw228資訊網——每日最新資訊28at.com

// 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...)        }                 // ... 執行 sql        result, err := db.Statement.ConnPool.ExecContext(            db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...,        )        // ... 獲取影響的行數        db.RowsAffected, _ = result.RowsAffected()                // ...    }}

6 刪除

圖片圖片tw228資訊網——每日最新資訊28at.com

接下來是數據記錄刪除流程,以 db.Delete 方法作為走讀入口:tw228資訊網——每日最新資訊28at.com

6.1 入口

在 db.Delete 方法中,核心步驟包括:tw228資訊網——每日最新資訊28at.com

  • ? 通過 db.getInstance() 方法獲取 db 的克隆實例
  • ? 通過 statement.AddClause(...) 方法追加使用方傳入的條件 condition
  • ? 設置 statement dest 為使用方傳入的 value
  • ? 獲取 delete 類型的 processor
  • ? 執行 processor.Execute(...) 方法,遍歷調用 fns 函數鏈
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 函數鏈中,最核心的函數是 Delete,其中的核心步驟包括:tw228資訊網——每日最新資訊28at.com

  • ? 調用 statement.Build(...) 方法,生成 sql
  • ? 倘若未啟用 AllowGlobalUpdate 模式,則會校驗使用方是否設置了 where 條件,未設置會拋出 gorm.ErrMissingWhereClause 錯誤(對應 checkMissingWhereConditions() 方法)
var ErrMissingWhereClause = errors.New("WHERE conditions required")
  • ? 調用 connPool.ExecContext(...) 方法,執行刪除操作(默認使用的是標準庫 database/sql 中的 db.ExecContxt(...) 方法)
  • ? 調用 result.RowsAffected() 方法,獲取本次刪除操作影響的數據行數
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()            }        }    }}

 tw228資訊網——每日最新資訊28at.com

checkMissingWhereConditions 方法的源碼如下:tw228資訊網——每日最新資訊28at.com

func checkMissingWhereConditions(db *gorm.DB) {    // 倘若 AllowGlobalUpdate 標識不為 true 且 error 為空,則需要對 where 條件進行校驗    if !db.AllowGlobalUpdate && db.Error == nil {        where, withCondition := db.Statement.Clauses["WHERE"]        // ...        // 不存在 where 條件,則需要拋出錯誤        if !withCondition {            db.AddError(gorm.ErrMissingWhereClause)        }        return    }}

7 更新

圖片圖片tw228資訊網——每日最新資訊28at.com

下面展示的是通過 gorm 更新數據的流程,以 db.Update(...) 方法作為源碼走讀的入口:tw228資訊網——每日最新資訊28at.com

7.1 入口

在 db.Update 方法中,核心步驟包括:tw228資訊網——每日最新資訊28at.com

? 通過 db.getInstance() 方法獲取 db 的克隆實例tw228資訊網——每日最新資訊28at.com

? 設置 statement dest 為使用方傳入的 valuetw228資訊網——每日最新資訊28at.com

? 獲取 update 類型的 processortw228資訊網——每日最新資訊28at.com

? 執行 processor.Execute(...) 方法,遍歷調用 fns 函數鏈tw228資訊網——每日最新資訊28at.com

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 函數鏈中,最核心的函數就是 Update,其中核心步驟包括:tw228資訊網——每日最新資訊28at.com

? 調用 statement.Build(...) 方法,生成 sqltw228資訊網——每日最新資訊28at.com

? 和 Delete 流程類似,倘若未啟用 AllowGlobalUpdate 模式,則會校驗使用方是否設置了 where 條件,未設置會拋出 gorm.ErrMissingWhereClause 錯誤tw228資訊網——每日最新資訊28at.com

? 調用 connPool.ExecContext(...) 方法,執行 sql(默認情況下,此處會使用 database/sql 標準庫的 db.ExecContext(...) 方法)tw228資訊網——每日最新資訊28at.com

? 調用 result.RowsAffected() 方法,獲取到本次更新操作影響的行數tw228資訊網——每日最新資訊28at.com

// 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...)        }                // ... 校驗 where 條件        checkMissingWhereConditions(db)        if !db.DryRun && db.Error == nil {            // ... 執行 sql            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()            }               }    }}

 tw228資訊網——每日最新資訊28at.com

8 事務

圖片圖片tw228資訊網——每日最新資訊28at.com

通過 gorm 框架同樣能夠很方便地使用事務相關的功能:tw228資訊網——每日最新資訊28at.com

? 調用 db.Transaction(...) 方法tw228資訊網——每日最新資訊28at.com

? 傳入閉包函數 fc,其中入參 tx 為帶有事務會話屬性的 db 實例,后續事務內所有執行操作都需要圍繞這個 tx 展開tw228資訊網——每日最新資訊28at.com

? 可以使用該 tx 實例完成事務的提交 tx.Commit() 和回滾 tx.Rollback() 操作tw228資訊網——每日最新資訊28at.com

8.1 入口

db.Transaction(...) 方法是啟動事務的入口:tw228資訊網——每日最新資訊28at.com

? 首先會調用 db.Begin(...) 方法啟動事務,此時會克隆出一個帶有事務屬性的 DB 會話實例:txtw228資訊網——每日最新資訊28at.com

? 以 tx 為入參,調用使用方傳入的閉包函數 fc(tx)tw228資訊網——每日最新資訊28at.com

? 倘若 fc 執行成功,則自動為用戶執行 tx.Commit() 操作tw228資訊網——每日最新資訊28at.com

? 倘若 fc 執行出錯或者發生 panic,則會 defer 保證執行 tx.Rollback() 操作tw228資訊網——每日最新資訊28at.com

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 {        // 開啟事務        tx := db.Begin(opts...)        if tx.Error != nil {            return tx.Error        }        defer func() {            // 倘若發生錯誤或者 panic,則進行 rollback 回滾            if panicked || err != nil {                tx.Rollback()            }        }()        // 執行事務內的邏輯        if err = fc(tx); err == nil {            panicked = false            // 指定成功會進行 commit 操作            return tx.Commit().Error        }    }    panicked = false    return}

 tw228資訊網——每日最新資訊28at.com

8.2 開啟事務

對于 DB.Begin() 方法,在默認模式下會使用 database/sql 庫下的 sql.DB.BeginTx 方法創建出一個 sql.Tx 對象,將其賦給當前事務會話 DB 的 statement.ConnPool 字段,以供后續使用:tw228資訊網——每日最新資訊28at.com

// 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) {    // 標準模式,會走到 sql.DB.BeginTX 方法    case TxBeginner:        // 創建好的 tx 賦給 statment.ConnPool        tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt)    // prepare 模式,會走到 PreparedStmtDB.BeginTx 方法中    case ConnPoolBeginner:        // 創建好的 tx 賦給 statment.ConnPool        tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt)    default:        err = ErrInvalidTransaction    }    if err != nil {        tx.AddError(err)    }    return tx}

 tw228資訊網——每日最新資訊28at.com

8.3 提交&回滾

事務的提交和回滾操作,會執行 statement 中的 connPool 的 Commit 和 Rollback 方法完成:tw228資訊網——每日最新資訊28at.com

? 執行事務提交操作:tw228資訊網——每日最新資訊28at.com

// Commit commits the changes in a transactionfunc (db *DB) Commit() *DB {    // 默認情況下,此處的 ConnPool 實現類為 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}

? 執行事務回滾操作:tw228資訊網——每日最新資訊28at.com

// Rollback rollbacks the changes in a transactionfunc (db *DB) Rollback() *DB {    // 默認情況下,此處的 ConnPool 實現類為 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}

 tw228資訊網——每日最新資訊28at.com

9 預處理

圖片圖片tw228資訊網——每日最新資訊28at.com

倘若創建 gorm.DB 時,倘若在 Config 中設置了 PrepareStmt 標識,則代表后續會啟用 prepare 預處理模式. 次吃,在執行 query 或者 exec 操作時,使用的 ConnPool 的實現版本是 PreparedStmtDB,執行時會拆分為兩個步驟:tw228資訊網——每日最新資訊28at.com

? 通過 PreparedStmtDB.prepare(...) 操作創建/復用 stmt,后續相同 sql 模板可以復用此 stmttw228資訊網——每日最新資訊28at.com

? 通過 stmt.Query(...)/Exec(...) 執行 sqltw228資訊網——每日最新資訊28at.com

9.1 prepare

在 PreparedStmtDB.prepare 方法中,會通過加鎖 double check 的方式,創建或復用 sql 模板對應的 stmt. 創建 stmt 的操作通過調用 conn.PrepareContext 方法完成.(通常此處的 conn 為 database/sql 庫下的 sql.DB)tw228資訊網——每日最新資訊28at.com

PreparedStmtDB.prepare 方法核心流程梳理如下:tw228資訊網——每日最新資訊28at.com

? 加讀鎖,然后以 sql 模板為 key,嘗試從 db.Stmts map 中獲取 stmt 復用tw228資訊網——每日最新資訊28at.com

? 倘若 stmt 不存在,則加寫鎖 double checktw228資訊網——每日最新資訊28at.com

? 調用 conn.PrepareContext(...) 方法,創建新的 stmt,并存放到 map 中供后續復用tw228資訊網——每日最新資訊28at.com

完整的代碼和對應的注釋展示如下:tw228資訊網——每日最新資訊28at.com

func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {    db.Mux.RLock()    // 以 sql 模板為 key,優先復用已有的 stmt     if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {        db.Mux.RUnlock()        // 并發場景下,只允許有一個 goroutine 完成 stmt 的初始化操作        <-stmt.prepared        if stmt.prepareErr != nil {            return Stmt{}, stmt.prepareErr        }        return *stmt, nil    }    db.Mux.RUnlock()    // 加鎖 double check,確認未完成 stmt 初始化則執行初始化操作    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    }    // 創建 stmt 實例,并添加到 stmts map 中    cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})}    db.Stmts[query] = &cacheStmt    // 此時可以提前解鎖是因為還通過 channel 保證了其他使用者會阻塞等待初始化操作完成    db.Mux.Unlock()    // 所有工作執行完之后會關閉 channel,喚醒其他阻塞等待使用 stmt 的 goroutine    defer close(cacheStmt.prepared)    // 調用 *sql.DB 的 prepareContext 方法,創建真正的 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}

 tw228資訊網——每日最新資訊28at.com

9.2 查詢

在 prepare 模式下,查詢操作通過 PreparedStmtDB.QueryContext(...) 方法實現. 首先通過 PreparedStmtDB.prepare(...) 方法嘗試復用 stmt,然后調用 stmt.QueryContext(...) 執行查詢操作.tw228資訊網——每日最新資訊28at.com

此處 stm.QueryContext(...) 方法本質上會使用 database/sql 中的 sql.Stmt 完成任務.tw228資訊網——每日最新資訊28at.com

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}

 tw228資訊網——每日最新資訊28at.com

9.3 執行

在 prepare 模式下,執行操作通過 PreparedStmtDB.ExecContext(...) 方法實現. 首先通過 PreparedStmtDB.prepare(...) 方法嘗試復用 stmt,然后調用 stmt.ExecContext(...) 執行查詢操作.tw228資訊網——每日最新資訊28at.com

此處 stm.ExecContext(...) 方法本質上會使用 database/sql 中的 sql.Stmt 完成任務.tw228資訊網——每日最新資訊28at.com

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}

 tw228資訊網——每日最新資訊28at.com

10 總結

本期主要和大家一起解析了 gorm 框架的底層實現原理.tw228資訊網——每日最新資訊28at.com

通篇學習下來,相信大家也能夠看出,gorm 框架名副其實,正是基于 orm 的思想,為使用方屏蔽了大量和 sql、db 有關的細節,讓使用方能夠像操作對象一樣完成和數據庫的交互操作.tw228資訊網——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-64098-0.htmlGorm 框架原理&amp;源碼解析

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 如何在 Asyncio 中使用 Socket

下一篇: Java注解和反射,你學會了嗎?

標簽:
  • 熱門焦點
  • K60至尊版狂暴引擎2.0加持:超177萬跑分斬獲性能第一

    Redmi的后性能時代戰略發布會今天下午如期舉辦,在本次發布會上,Redmi公布了多項關于和聯發科的深度合作,以及新機K60 Ultra在軟件和硬件方面的特性,例如:“K60 至尊版,雙芯旗艦
  • 天貓精靈Sound Pro體驗:智能音箱沒有音質?來聽聽我的

    這幾年除了手機作為智能生活終端最主要的核心之外,第二個可以成為中心點的產品是什么?——是智能音箱。 手機在執行命令的時候有兩種操作方式,手和智能語音助手,而智能音箱只
  • 6月iOS設備性能榜:M2穩居榜首 A系列只能等一手3nm來救

    沒有新品發布,自然iOS設備性能榜的上榜設備就沒有什么更替,僅僅只有跑分變化而產生的排名變動,畢竟蘋果新品的發布節奏就是這樣的,一年下來也就幾個移動端新品,不會像安卓廠商,一
  • 6月安卓手機好評榜:魅族20 Pro蟬聯冠軍

    性能榜和性價比榜之后,我們來看最后的安卓手機好評榜,數據來源安兔兔評測,收集時間2023年6月1日至6月30日,僅限國內市場。第一名:魅族20 Pro好評率:95%5月份的時候魅族20 Pro就是
  • 5月iOS設備好評榜:iPhone 14僅排第43?

    來到新的一月,安兔兔的各個榜單又重新匯總了數據,像安卓陣營的榜單都有著比較大的變動,不過iOS由于設備的更新換代并沒有那么快,所以相對來說變化并不大,特別是iOS好評榜,老款設
  • Rust中的高吞吐量流處理

    作者 | Noz編譯 | 王瑞平本篇文章主要介紹了Rust中流處理的概念、方法和優化。作者不僅介紹了流處理的基本概念以及Rust中常用的流處理庫,還使用這些庫實現了一個流處理程序
  • Golang 中的 io 包詳解:組合接口

    io.ReadWriter// ReadWriter is the interface that groups the basic Read and Write methods.type ReadWriter interface { Reader Writer}是對Reader和Writer接口的組合,
  • 阿里大調整

    來源:產品劉有媒體報道稱,近期淘寶天貓集團啟動了近年來最大的人力制度改革,涉及員工績效、層級體系等多個核心事項,目前已形成一個初步的&ldquo;征求意見版&rdquo;:1、取消P序列
  • iQOO Neo8 Pro搶先上架:首發天璣9200+ 安卓性能之王

    經過了一段時間的密集爆料,昨日iQOO官方如期對外宣布:將于5月23日推出全新的iQOO Neo8系列新品,官方稱這是一款擁有旗艦級性能調校的作品。隨著發布時
Top 主站蜘蛛池模板: 永善县| 保德县| 林口县| 广平县| 屯留县| 鱼台县| 康乐县| 新野县| 涞源县| 萝北县| 封开县| 曲沃县| 泌阳县| 阿尔山市| 漳浦县| 聊城市| 井冈山市| 通河县| 扶沟县| 北宁市| 邮箱| 霍城县| 四子王旗| 琼海市| 区。| 达日县| 互助| 成安县| 新河县| 封开县| 西和县| 绥中县| 隆德县| 鹿泉市| 紫云| 伊春市| 丹阳市| 麻阳| 虹口区| 昭平县| 宜宾县|