gorm 框架原理 - 源碼解析
0 前言
本篇將和大家探討 go 語言中最流行的 orm 框架 ——gorm 的底層實現原理.
那麼步入正題,本篇分享內容的目錄大綱如下所示:
1 入口
gorm 框架是國內的大神 jinzhu 基於 go 語言開源實現的一款數據庫 orm 框架. 【gorm】一詞恢弘大氣,前綴 go 代表 go 語言, 後綴 orm 全稱 Object Relation Mapping,指的是使用對象映射的方式,讓使用方能夠像操作本地對象實例一樣輕鬆便捷地完成遠端數據庫的操作.
gorm 框架開源地址爲: https://github.com/go-gorm/gorm
本期會涉及到大量 gorm 的源碼走讀環節,使用的代碼版本爲 tag: v.1.25.5
下面我們簡單回顧一下針對 gorm 框架的常規用法:
1.1 初始化
gorm 框架通過一個 gorm.DB 實例來指代我們所操作的數據庫. 使用 gorm 的第一步就是要通過 Open 方法創建出一個 gorm.DB 實例,其中首個入參爲連接器 dialector,本身是個抽象的 interface,其實現類關聯了具體數據庫類型.
本文將統一以 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(){
// 創建 db 實例
db, err = gorm.Open(mysql.Open(dsn),&gorm.Config{})
})
return db,err
}
本文將以 gorm.Open 方法爲入口,在第 3 章中深入源碼底層鏈路.
1.2 po 模型
基於 orm 的思路,與某張數據表所關聯映射的是 po (persist object) 模型.
定義 po 類時,可以通過聲明 TableName 方法,來指定該類對應的表名.
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 類時,可以通過組合 gorm.Model 的方式,完成主鍵、增刪改時間等 4 列信息的一鍵添加,並且由於聲明瞭 DeletedAt 字段,gorm 將會默認會啓動軟刪除模式. (有關軟刪除的內容,可參見前文——gorm 框架使用教程)
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
1.3 查詢
下面展示的是使用 gorm 進行數據查詢操作的代碼示例. 本文第 4 章會以 db.First(...) 方法爲入口,展開底層源碼鏈路的走讀.
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(...) 方法爲入口,展開底層源碼鏈路的走讀.
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(...) 方法爲入口,展開底層源碼鏈路的走讀.
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(...) 方法爲入口,展開底層源碼鏈路的走讀.
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(...) 方法爲入口,展開底層源碼鏈路的走讀.
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
}
}
2 核心類
本章中,我會首先向大家介紹 gorm 框架中各個核心類的定義.
2.1 數據庫
gorm.DB 是 gorm 定義的數據庫類. 所有執行的數據庫的操作都將緊密圍繞這個類,以鏈式調用的方式展開. 每當執行過鏈式調用後,新生成的 DB 對象中就存儲了一些當前請求特有的狀態信息,我們把這種對象稱作 “會話”.
DB 類中的核心字段包括:
-
• Config:用戶自定義的配置項
-
• Error:一次會話執行過程中遇到的錯誤
-
• RowsAffected:該請求影響的行數
-
• Statement:一次會話的狀態信息,比如請求和響應信息
-
• clone:會話被克隆的次數. 倘若 clone = 1,代表是始祖 DB 實例;倘若 clone > 1,代表是從始祖 DB 克隆出來的會話
DB 類定義的代碼如下:
// gorm 中定義的數據庫類
// 所有 orm 的思想
type DB struct {
// 配置
*Config
// 錯誤
Error error
// 影響的行數
RowsAffected int64
// 會話狀態信息
Statement *Statement
// 克隆次數
clone int
}
I 錯誤處理
DB 類的 AddError 方法,用於在會話執行過程中拋出錯誤.
一次會話在執行過程中可能會遇到多個錯誤,因此會通過 error wrapping 的方式,實現錯誤的拼接.
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 表名設置
請求在執行時,需要明確操作的是哪張數據表.
使用方可以通過鏈式調用 DB.Table 方法,顯式聲明本次操作所針對的數據表,這種方式的優先級是最高的.
func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
tx = db.getInstance()
// ...
tx.Statement.Table = name
// ...
return
}
在 DB.Table 方法缺省的情況下,gorm 則會嘗試通過 po 類的 TableName 方法獲取表名.
在 gorm 中聲明瞭一個 tabler interface:
type Tabler interface {
TableName() string
}
倘若 po 模型聲明瞭 TableName 方法,則隱式實現了該 interface,在處理過程中會被斷言成 tabler 類型,然後調用 TableName 方法獲取其表名.
該流程對應的源碼展示如下:
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,然後調用 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 語句拼接格式、響應參數類型、數據表的名稱等等.
statement 類中涉及到的各個核心字段通過下方的代碼和註釋加以介紹:
// Statement statement
type 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,定義如下:
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 預處理模式,存在不同的實現類版本:
-
• ** 在普通模式下,connPool 的實現類爲 database/sql 庫下的 *DB 類 **(詳細內容參見前文——Golang sql 標準庫源碼解析)
-
• 在 prepare 模式下,connPool 實現類型爲 gorm 中定義的 PreparedStmtDB 類,在本文 2.3 小節中展開
II db 克隆
此處額外介紹一下 DB 的克隆流程,所有在始祖 DB 基礎上追加狀態信息,克隆出來的 DB 實例都可以稱爲 “會話”.
會話的狀態信息主要存儲在 statement 當中的,所以在克隆 DB 時,很重要的一環就是完成對 其中 statement 部分的創建 / 克隆.
該流程對應的方法爲 DB.getInstance 方法,主要通過 DB 中的 clone 字段來判斷當前是首次從始祖 DB 中執行克隆操作還是在一個會話的基礎上克隆出一個新的會話實例,對應的源碼展示如下:
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
}
2.3 預處理 DB
在 prepare 預處理模式下,DB 中連接池 connPool 的實現類爲 PreparedStmtDB. 定義該類的目的是爲了使用 database/sql 標準庫中的 prepare 能力,完成預處理狀態 statement 的構造和複用.
PreparedStmtDB 的類定義如下:
// 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 類的簡單封裝,兩者區別並不大:
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 字段當中.
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 方法中.
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:
func (cs *callbacks) Query() *processor {
return cs.processors["query"]
}
執行器 processor 具體的類定義如下,其中核心字段包括:
-
• db:從屬的 gorm.DB 實例
-
• Clauses:根據 crud 類型確定的 SQL 格式模板,後續用於拼接生成 sql
-
• fns:對應於 crud 類型的執行函數鏈
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
}
所有請求遵循的處理思路都是,首先根據其從屬的 crud 類型,找到對應的 processor,然後調用 processor 的 Execute 方法,執行該 processor 下的 fns 函數鏈.
這一點,在接下來 4、5、 6、 7 章中介紹的 crud 流程都是如此.
// 通用的 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 格式展示如下:
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.
當使用方通過鏈式操作克隆 DB 時,對應追加的狀態信息就會生成一個新的 clause,追加到 statement 對應的 clauses 集合當中. 當請求實際執行時,會取出 clauses 集合,拼接生成完整的 sql 用於執行.
條件 clause 本身是個抽象的 interface,定義如下:
// Interface clause interface
type Interface interface {
// clause 名稱
Name() string
// 生成對應的 sql 部分
Build(Builder)
// 和同類 clause 合併
MergeClause(*Clause)
}
不同的 clause 有不同的實現類,我們以 SELECT 爲例進行展示:
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.
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 字段中.
以 query 流程爲例,拼接 sql 的流程入口可以參見 4.3 小節代碼展示當中的 BuildQuerySQL(...) 方法.
3 初始化
本章中,我們將會以 gorm.Open 方法作爲入口,詳細展開創建 gorm.DB 實例的源碼細節.
3.1 創建 db
gorm.Open 方法是創建 DB 實例的入口方法,其中包含如下幾項核心步驟:
-
• 完成 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
}
3.2 初始化 dialector
mysql 是我們常用的數據庫,對應於 mysql 版本的 dialector 實現類位於 github.com/go-sql-driver/mysql 包下. 使用方可以通過 Open 方法,將傳入的 dsn 解析成配置,然後返回 mysql 版本的 Dialector 實例.
package mysql
func 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 的註冊操作:
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 函數
對應於 crud 四類 processor,註冊的函數鏈 fns 的內容和順序是固定的,展示如上圖. 相應的源碼展示如下,對應的方法爲 RegisterDefaultCallbacks(...):
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 方法,對應的核心源碼鏈路展示如下:
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 方法作爲入口,展示數據庫查詢的方法鏈路:
4.1 入口
在 db.First 方法當中:
-
• 遵循 First 的語義,通過 limit 和 order 追加 clause,限制只取滿足條件且主鍵最小的一筆數據
-
• 追加用戶傳入的一系列 condition,進行 clause 追加
-
• 在 First、Take、Last 等方法中,會設置 RaiseErrorOnNotFound 標識爲 true,倘若未找到記錄,則會拋出 ErrRecordNotFound 錯誤
var ErrRecordNotFound = logger.ErrRecordNotFound
-
• 設置 statement 中的 dest 爲用戶傳入的 dest,作爲反序列化響應結果的對象實例
-
• 獲取 query 類型的 processor,調用 Execute 方法執行其中的 fn 函數鏈,完成 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 添加條件
執行查詢類操作時,通常會通過鏈式調用的方式,傳入一些查詢限制條件,比如 Where、Group By、Order、Limit 之類. 我們以 Limit 爲例,進行展開介紹:
-
• 首先調用 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
}
4.3 核心方法
在 query 類型 processor 的 fns 函數鏈中,最主要的函數是 Query,其中涉及的核心步驟包括:
-
• 調用 BuildQuerySQL(...) 方法,根據傳入的 clauses 組裝生成 sql
-
• 調用 connPool.QueryContext(...) ,完成查詢類 sql 的執行,返回查到的行數據 rows(非 prepare 模式下,此處會對接 database/sql 庫,走到 sql.DB.QueryContext(...) 方法中)
-
• 調用 gorm.Scan() 方法,將結果數據反序列化到 statement 的 dest 當中
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 掃描數據
接下來展示一下,gorm.Scan() 方法,其作用是將查詢結果數據反序列化到 dest 當中:
-
• 通過對 statement 中的 dest 進行分類,採取的不同的處理方式
-
• 核心方法都是通過 rows.Scan(...) 方法,將響應數據反序列化到 dest 當中
-
• 調用 rows.Err() 方法,拋出請求過程中遇到的錯誤
-
• 倘若啓用了 RaiseErrorOnNotFound 模式且查詢到的行數爲 0,則拋出錯誤 ErrRecordNotFound
對應源碼展示如下:
// 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 創建
本章以 db.Create(...) 方法爲入口,展開介紹一下創建數據記錄的流程.
5.1 入口
創建數據記錄操作主要通過調用 gorm.DB 的 Create 方法完成,其包括如下核心步驟:
-
• 通過 db.getInstance() 克隆出一個 DB 會話實例
-
• 設置 statement 中的 dest 爲用戶傳入的 dest
-
• 獲取到 create 類型的 processor
-
• 調用 processor 的 Execute 方法,遍歷執行 fns 函數鏈,完成創建操作
// Create inserts value, returning the inserted data's primary key in value's id
func (db *DB) Create(value interface{}) (tx *DB) {
// ...
// 克隆 db 會話實例
tx = db.getInstance()
// 設置 dest
tx.Statement.Dest = value
// 執行 create processor
return tx.callbacks.Create().Execute(tx)
}
5.2 核心方法
在 create 類型 processor 的 fns 函數鏈中,最主要的執行函數就是 Create,其中核心步驟包括:
-
• 調用 statement.Build(...) 方法,生成 sql
-
• 調用 connPool.ExecContext(...) 方法,請求 mysql 服務端執行 sql(默認情況下,此處會使用 database/sql 標準庫的 db.ExecContext(...) 方法)
-
• 調用 result.RowsAffected() ,獲取到本次創建操作影響的數據行數
// 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...)
}
// ... 執行 sql
result, err := db.Statement.ConnPool.ExecContext(
db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...,
)
// ... 獲取影響的行數
db.RowsAffected, _ = result.RowsAffected()
// ...
}
}
6 刪除
接下來是數據記錄刪除流程,以 db.Delete 方法作爲走讀入口:
6.1 入口
在 db.Delete 方法中,核心步驟包括:
-
• 通過 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,其中的核心步驟包括:
-
• 調用 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()
}
}
}
}
checkMissingWhereConditions 方法的源碼如下:
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 更新
下面展示的是通過 gorm 更新數據的流程,以 db.Update(...) 方法作爲源碼走讀的入口:
7.1 入口
在 db.Update 方法中,核心步驟包括:
-
• 通過 db.getInstance() 方法獲取 db 的克隆實例
-
• 設置 statement dest 爲使用方傳入的 value
-
• 獲取 update 類型的 processor
-
• 執行 processor.Execute(...) 方法,遍歷調用 fns 函數鏈
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,其中核心步驟包括:
-
• 調用 statement.Build(...) 方法,生成 sql
-
• 和 Delete 流程類似,倘若未啓用 AllowGlobalUpdate 模式,則會校驗使用方是否設置了 where 條件,未設置會拋出 gorm.ErrMissingWhereClause 錯誤
-
• 調用 connPool.ExecContext(...) 方法,執行 sql(默認情況下,此處會使用 database/sql 標準庫的 db.ExecContext(...) 方法)
-
• 調用 result.RowsAffected() 方法,獲取到本次更新操作影響的行數
// 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...)
}
// ... 校驗 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()
}
}
}
}
8 事務
通過 gorm 框架同樣能夠很方便地使用事務相關的功能:
-
• 調用 db.Transaction(...) 方法
-
• 傳入閉包函數 fc,其中入參 tx 爲帶有事務會話屬性的 db 實例,後續事務內所有執行操作都需要圍繞這個 tx 展開
-
• 可以使用該 tx 實例完成事務的提交 tx.Commit() 和回滾 tx.Rollback() 操作
8.1 入口
db.Transaction(...) 方法是啓動事務的入口:
-
• 首先會調用 db.Begin(...) 方法啓動事務,此時會克隆出一個帶有事務屬性的 DB 會話實例:tx
-
• 以 tx 爲入參,調用使用方傳入的閉包函數 fc(tx)
-
• 倘若 fc 執行成功,則自動爲用戶執行 tx.Commit() 操作
-
• 倘若 fc 執行出錯或者發生 panic,則會 defer 保證執行 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 {
// 開啓事務
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
}
8.2 開啓事務
對於 DB.Begin() 方法,在默認模式下會使用 database/sql 庫下的 sql.DB.BeginTx 方法創建出一個 sql.Tx 對象,將其賦給當前事務會話 DB 的 statement.ConnPool 字段,以供後續使用:
// 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) {
// 標準模式,會走到 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
}
8.3 提交 & 回滾
事務的提交和回滾操作,會執行 statement 中的 connPool 的 Commit 和 Rollback 方法完成:
- • 執行事務提交操作:
// Commit commits the changes in a transaction
func (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
}
- • 執行事務回滾操作:
// Rollback rollbacks the changes in a transaction
func (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
}
9 預處理
倘若創建 gorm.DB 時,倘若在 Config 中設置了 PrepareStmt 標識,則代表後續會啓用 prepare 預處理模式. 次喫,在執行 query 或者 exec 操作時,使用的 ConnPool 的實現版本是 PreparedStmtDB,執行時會拆分爲兩個步驟:
-
• 通過 PreparedStmtDB.prepare(...) 操作創建 / 複用 stmt,後續相同 sql 模板可以複用此 stmt
-
• 通過 stmt.Query(...)/Exec(...) 執行 sql
9.1 prepare
在 PreparedStmtDB.prepare 方法中,會通過加鎖 double check 的方式,創建或複用 sql 模板對應的 stmt. 創建 stmt 的操作通過調用 conn.PrepareContext 方法完成.(通常此處的 conn 爲 database/sql 庫下的 sql.DB)
PreparedStmtDB.prepare 方法核心流程梳理如下:
-
• 加讀鎖,然後以 sql 模板爲 key,嘗試從 db.Stmts map 中獲取 stmt 複用
-
• 倘若 stmt 不存在,則加寫鎖 double check
-
• 調用 conn.PrepareContext(...) 方法,創建新的 stmt,並存放到 map 中供後續複用
完整的代碼和對應的註釋展示如下:
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
}
9.2 查詢
在 prepare 模式下,查詢操作通過 PreparedStmtDB.QueryContext(...) 方法實現. 首先通過 PreparedStmtDB.prepare(...) 方法嘗試複用 stmt,然後調用 stmt.QueryContext(...) 執行查詢操作.
此處 stm.QueryContext(...) 方法本質上會使用 database/sql 中的 sql.Stmt 完成任務.
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 執行
在 prepare 模式下,執行操作通過 PreparedStmtDB.ExecContext(...) 方法實現. 首先通過 PreparedStmtDB.prepare(...) 方法嘗試複用 stmt,然後調用 stmt.ExecContext(...) 執行查詢操作.
此處 stm.ExecContext(...) 方法本質上會使用 database/sql 中的 sql.Stmt 完成任務.
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 總結
本期主要和大家一起解析了 gorm 框架的底層實現原理.
通篇學習下來,相信大家也能夠看出,gorm 框架名副其實,正是基於 orm 的思想,爲使用方屏蔽了大量和 sql、db 有關的細節,讓使用方能夠像操作對象一樣完成和數據庫的交互操作.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/STFnyke1NX8Ag8COlHwaLA