深入理解gorm是如何和數(shù)據(jù)庫建立連接的
大家好,我是漁夫子。
本期和大家一起學(xué)習(xí)下gorm是如何和數(shù)據(jù)庫建立連接的。
一、gorm.Open
通常情況下,我們是通過gorm.Open函數(shù)就能在應(yīng)用層和數(shù)據(jù)建立連接。如下:
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
在該代碼片段中,我們傳入了數(shù)據(jù)庫的用戶名、密碼、地址以及數(shù)據(jù)庫和數(shù)據(jù)庫對應(yīng)的配置。然后通過gorm.Open函數(shù)就和數(shù)據(jù)庫建立連接了,gorm.Open函數(shù)返回的是一個gorm.DB對象。如下:
// DB GORM DB definition
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}
在該數(shù)據(jù)結(jié)構(gòu)中并沒有和數(shù)據(jù)庫連接相關(guān)的字段,那gorm.Open到底是如何和mysql數(shù)據(jù)庫建立連接的呢?我們繼續(xù)深入gorm.Open函數(shù)和mysql.Open函數(shù)的詳細內(nèi)容。
二、gorm.Open函數(shù)
在gorm.Open函數(shù)中,傳入的參數(shù)是一個Dialector?接口類型的dialector變量。我們看到會將傳入的Dialector變量賦值給配置config.Dialector,如下:
config.Dialector = dialector
然后,又通過config.Dialector的Initialize函數(shù)對數(shù)據(jù)庫進行了初始化。如下:
err = config.Dialector.Initialize(db)
那么,Dialector是什么呢?Dialector是通過gorm.Open函數(shù)的第一個參數(shù)傳進來的。我們看具體的是什么。
三、Dialector參數(shù)
在gorm.Open函數(shù)中,第一個參數(shù)是Dialector類型的參數(shù),這是一個接口類型。也就是說只要實現(xiàn)了該接口,就能作為一個Dialector。這也就是gorm能夠針對很多數(shù)據(jù)庫進行操作的原因。比如MySQL、ClickHouse等。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
}
具體到mysql的數(shù)據(jù)庫,我們看到是通過gorm.io/driver/mysql?庫的Open函數(shù)來初始化的。我們看下mysql.Open函數(shù)的實現(xiàn),如下:
func Open(dsn string) gorm.Dialector {
dsnConf, _ := mysql.ParseDSN(dsn)
return &Dialector{Config: &Config{DSN: dsn, DSNConfig: dsnConf}}
}
該函數(shù)接收一個dsn的字符串,也就是第一節(jié)中我們提供的和數(shù)據(jù)庫相關(guān)的賬號密碼等連接數(shù)據(jù)的信息。然后,返回的是mysql驅(qū)動包中的Dialector對象。該對象包含了相關(guān)的配置。
然后,是在gorm.Open函數(shù)中,調(diào)用了Dialector的Initialize?函數(shù)。我們看下該函數(shù)中和數(shù)據(jù)庫連接相關(guān)的邏輯。
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
if dialector.DriverName == "" {
dialector.DriverName = "mysql"
}
if dialector.DefaultDatetimePrecision == nil {
dialector.DefaultDatetimePrecision = &defaultDatetimePrecision
}
if dialector.Conn != nil {
db.ConnPool = dialector.Conn
} else {
db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
if err != nil {
return err
}
}
// 省略其他代碼
}
大家看到,在第13行的地方,是通過sql.Open函數(shù)來進行具體的和數(shù)據(jù)庫進行連接的。然后返回的對象是sql.DB類型,大家注意,這里的sql.DB類型是go標準庫中的DB,而非gorm庫中的DB。返回的sql.DB對象賦值給了gorm中DB對象中的ConnPool。
同時,在gorm.Open函數(shù)中,還將db.ConnPool對象賦值給了db.Statement.ConnPool對象。到這里是不是gorm.DB結(jié)構(gòu)體中的字段就和數(shù)據(jù)庫的具體連接關(guān)聯(lián)起來。
接下來,我們再看看sql.Open函數(shù)是如何和數(shù)據(jù)庫建立連接的。
四、sql.Open函數(shù)
先看sql.Open函數(shù)的源代碼:
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
}
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
我們先簡單分析下上述代碼。在第3行處,從drivers中獲取對應(yīng)的驅(qū)動名稱的具體驅(qū)動對象。這里的driverName是mysql。然后從第9行到第14行是執(zhí)行具體驅(qū)動程序的連接函數(shù)。
首先,我們先看從drivers中根據(jù)驅(qū)動名稱mysql獲取驅(qū)動對象的邏輯。drivers是標準庫sql中的一個map類型,如下:
drivers = make(map[string]driver.Driver)
該變量是通過sql包中的Register函數(shù)進行注冊的:
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
該函數(shù)又是在哪里進行調(diào)用的呢?我們再回調(diào)gorm.Open函數(shù)中,第一個參數(shù)調(diào)用的是mysql.Open函數(shù)。也就是說引入了庫gorm.io/driver/mysql?,在該庫中,我們看到又引入了github.com/go-sql-driver/mysql?庫。該庫中有一個init方法,如下:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
原來,這里調(diào)用了標準庫sql中的Register函數(shù),將“mysql”和對應(yīng)的驅(qū)動對象MySQLDriver進行了注冊關(guān)聯(lián)。
我們再返回來看sql.Open函數(shù)的具體實現(xiàn)。那這里就繼續(xù)調(diào)用MySQLDriver的OpenConnector方法。我們看下該方法的實現(xiàn)如下:
// OpenConnector implements driver.DriverContext.
func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
return &connector{
cfg: cfg,
}, nil
}
該函數(shù)首先通過ParseDSN解析dsn字符串中的用戶名,地址,密碼等配置選項。然后返回一個connector對象。該connector對象就是在sql.Open函數(shù)中執(zhí)行的OpenDB(connector)函數(shù)中的參數(shù)。
我們繼續(xù)看sql.OpenDB函數(shù)的實現(xiàn),如下:
func OpenDB(c driver.Connector) *DB {
ctx, cancel := context.WithCancel(context.Background())
db := &DB{
connector: c,
openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string),
connRequests: make(map[uint64]chan connRequest),
stop: cancel,
}
go db.connectionOpener(ctx)
return db
}
這里首先構(gòu)建了一個sql.DB對象,同時執(zhí)行了一個協(xié)程進行數(shù)據(jù)庫的連接:
go db.connectionOpener(ctx)
接著看db.connectionOpener函數(shù)的實現(xiàn),如下:
// Runs in a separate goroutine, opens new connections when requested.
func (db *DB) connectionOpener(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-db.openerCh:
db.openNewConnection(ctx)
}
}
}
這里,有一個db.openNewConnection函數(shù),根據(jù)名字可知是打開新的連接。其實現(xiàn)如下:
// Open one new connection
func (db *DB) openNewConnection(ctx context.Context) {
ci, err := db.connector.Connect(ctx)
// ...省略代碼
dc := &driverConn{
db: db,
createdAt: nowFunc(),
returnedAt: nowFunc(),
ci: ci,
}
if db.putConnDBLocked(dc, err) {
db.addDepLocked(dc, dc)
} else {
db.numOpen--
ci.Close()
}
}
這里我們看到有一個db.connector.Connect函數(shù),connector就是github.com/go-sql-driver/mysql?庫中的connector對象。我們回到該庫,查看其Connect函數(shù)的實現(xiàn):
// Connect implements driver.Connector interface.
// Connect returns a connection to the database.
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
// New mysqlConn
mc := &mysqlConn{
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}),
cfg: c.cfg,
}
mc.parseTime = mc.cfg.ParseTime
// Connect to Server
dialsLock.RLock()
dial, ok := dials[mc.cfg.Net]
dialsLock.RUnlock()
if ok {
//...省略代碼
} else {
nd := net.Dialer{Timeout: mc.cfg.Timeout}
mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
}
// Enable TCP Keepalives on TCP connections
if tc, ok := mc.netConn.(*net.TCPConn); ok {
if err := tc.SetKeepAlive(true); err != nil {
//...省略代碼
}
}
mc.buf = newBuffer(mc.netConn)
//...
// Reading Handshake Initialization Packet
authData, plugin, err := mc.readHandshakePacket()
if err != nil {
mc.cleanup()
return nil, err
}
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil {
mc.cleanup()
return nil, err
}
// Handle response to auth packet, switch methods if possible
if err = mc.handleAuthResult(authData, plugin); err != nil {
mc.cleanup()
return nil, err
}
return mc, nil
}
這里我們主要看第22到23行,這里進行了實際的撥號操作,也就是和數(shù)據(jù)庫真正的建立了連接。再看第27行,斷言是一個TCP連接。第37行,進行了握手處理;第45行,進行了認證處理。最終返回了一個mysqlConn對象。該mysqlConn結(jié)構(gòu)體中包含字段如下:
type mysqlConn struct {
buf buffer
netConn net.Conn
rawConn net.Conn // underlying connection when netConn is TLS connection.
// ...
}
其中,netConn就是和數(shù)據(jù)庫建立的TCP的連接。
五、從mysql到gorm.DB
我們再總結(jié)下上述和mysql相關(guān)的各個對象之間的關(guān)聯(lián)關(guān)系。從mysql開始逆向推導(dǎo)。如下:
也就是說,我們在使用gorm進行數(shù)據(jù)庫操作的時候,最終都是從gorm.Statement.ConnPool中獲取的數(shù)據(jù)庫連接來具體執(zhí)行sql語句的。
好了,今天gorm是如何和mysql建立連接的過程就跟大家分享到這里。