Gorm原始碼學習-資料庫連線

2022-11-22 06:01:00

1 前言

gorm原始碼地址: Gorm , 本文基於commit:cef3de694d9615c574e82dfa0b50fc7ea2816f3e

官方入門指南: Doc


 

2 連線資料庫程式碼範例

目前Gorm官方支援的資料庫型別有:MySQL, PostgreSQL, SQLite, SQL Server. 

目前Go官方支援MySQL驅動,程式碼地址:mysql-driver

下面來看連線MySQL的資料庫的基本程式碼

package main

import (
	"fmt"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	// 參考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 獲取詳情
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?timeout=%s&readTimeout=%s&writeTimeout=%s",
		"root", "zbwmysql", "127.0.0.1", "3306", "user_db", "100ms", "2s", "3s")
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Printf("gorm open fail, err:%v dsn:%v\n", err, dsn)
	}
	mysqlDB, err := db.DB()
	if err != nil {
		fmt.Printf("get mysql db fail, err:%v\n", err)
	}
	// 參考 https://github.com/go-sql-driver/mysql#important-settings 獲取詳情
	mysqlDB.SetConnMaxLifetime(time.Minute * 3) // 使用者端將空閒連線主動斷開的超時時間,官方建議小於5分鐘
	mysqlDB.SetMaxOpenConns(10)                 // 取決於伺服器的設定
	mysqlDB.SetMaxIdleConns(10)                 // 官方建議和SetMaxOpenConns相同
}

這裡有必要看下 timeoutreadTimeoutwriteTimeoutSetConnMaxLifetime 三個引數

timeout是指 建立連線的一個超時時間

readTimeout是指 I/O 讀操作的超時時間

writeTimeout是指 I/O 寫操作的超時時間

SetConnMaxLifetime 是指使用者端將空閒連線主動斷開的超時時間,

如果設定為0,則連線池的連線將這一直被複用,但是系統會主動將長時間的連線殺掉,

因此若使用者端再次使用長時間空閒的連線將會報錯,driver: bad connection,具體如下

問題的修復記錄,可以看 issues-1120 

 

 3 連線資料庫程式碼分析

從上一節看,Gorm連線資料庫的過程只需要呼叫一個函數,

func Open(dialector Dialector, opts ...Option) (db *DB, err error)

 但是Gorm目前是MySQL, PostgreSQL, SQLite, SQL Server,四種型別的資料庫的,這個是怎麼做到的呢?

 這就需要具體看下請求引數Dialector 和 返回引數DBOpen函數內部實現

 首先讓我們先看看Golang的interface型別

 

3.1 interface理解

An interface type is defined as a set of method signatures.

A value of interface type can hold any value that implements those methods.

A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword.

 以上摘抄自A Tour of Gointerface是一種包含方法定義的型別,通過實現該interface的所有方法來隱式實現該介面。

 

 3.2 Dialector介面定義

Dialector定義如下,這裡對部分方法加了註釋,方便理解。

// Dialector GORM database dialector
type Dialector interface {
	Name() string // 驅動名稱
	Initialize(*DB) error // 初始化連線
	Migrator(db *DB) Migrator 
	DataTypeOf(*schema.Field) string // 型別對映
	DefaultValueOf(*schema.Field) clause.Expression // 型別預設值
	BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
	QuoteTo(clause.Writer, string)
	Explain(sql string, vars ...interface{}) string // SQL語句格式化輸出
}

不僅僅Mysql驅動,PostgreSQL, SQLite, SQL Server驅動都得實現Dialector中定義的全部方法。

並且這些方法恰恰是不同資料庫的區別所在,比如不同資料庫的資料型別是有差異的,即使含義相同,寫法也可能不同,

因此gorm的資料型別對映到不同資料庫能識別的型別,這就是DataTypeOf實現的功能。

可以在go-gorm找到各種資料庫的Dialector實現,如PostgreSQL DialectorMySQL Dialector 

 

3.3 DB結構體定義

DB定義如下,這裡對部分方法加了註釋,方便理解。

// DB GORM DB definition
type DB struct {
	*Config // 連線及其連線相關資訊等
	Error        error 
	RowsAffected int64 
	Statement    *Statement // SQL語句執行相關資訊
	clone        int
}

 其中,Config中會保留連線ConnPool 、CRUD相關的函數callbacks等資訊,部分程式碼程式碼如下,完整程式碼見gorm.Config

// Config GORM config
type Config struct {
	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool
	ConnPool ConnPool
	// Dialector database dialector
	Dialector
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks
	cacheStore *sync.Map
}

看了Open函數的請求引數和返回引數,接下來我們看看內部的具體實現

 

3.3 Gorm.Open實現分析

Gorm.Open完整程式碼可以在Github上看到。這裡重點關注兩個地方

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},
		},
	}
}
	if config.Dialector != nil {
		err = config.Dialector.Initialize(db)
	}

這裡會根據具體Dialector的具體值呼叫對應的Initialize方法。

如果Dialector 為 mysql.Open(dsn)的返回值,那就會呼叫Gorm MySQL驅動的Initialize方法。

 

3.4MySQL DialectorInitialize 方法實現分析

Initialize 主要乾了兩件事情,呼叫sql.Open、註冊CRUD的處理常式及對應的勾點函數。

勾點函數是在建立、查詢、更新、刪除等操作之前、之後呼叫的函數。

 

3.4.1 呼叫sql.Open,該函數可能只是校驗下引數,並沒有實際建立連線

db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)

其中,sql.Open宣告如下

func Open(driverName, dataSourceName string) (*DB, error)

db.ConnPoolinterface型別,定義如下

// ConnPool db conns pool 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
}

sql.DB是結構體型別,gorm.ConnPoolinterface型別,因此sql.DB實現了gorm.ConnPool定義的四個方法,因此CRUD操作會通過gorm.ConnPool呼叫到sql.DB實現的這四個函數實現。

這裡也應證了前面的說明

A value of interface type can hold any value that implements those methods.

通過看原始碼sql.Connsql.Tx也實現了gorm.ConnPool定義的四個方法。

 

3.4.2 註冊CRUD相關函數,這裡只擷取callbacks.RegisterDefaultCallbacks的部分實現。

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

在gorm.Open的過程中註冊了建立記錄時的回撥函數createCallback.Register("gorm:create", Create(config))
具體的細節在後續章節展開,這裡就細說。

此外,從程式碼可以看出,這裡註冊了在建立操作之前、之後呼叫的勾點方法。