深入理解gorm如何和數(shù)據(jù)庫(kù)建立連接
一、gorm.Open
通常情況下,我們是通過(guò)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ù)庫(kù)的用戶名、密碼、地址以及數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)對(duì)應(yīng)的配置。然后通過(guò)gorm.Open函數(shù)就和數(shù)據(jù)庫(kù)建立連接了,gorm.Open函數(shù)返回的是一個(gè)gorm.DB對(duì)象。如下:
// DB GORM DB definition type DB struct { *Config Error error RowsAffected int64 Statement *Statement clone int }
在該數(shù)據(jù)結(jié)構(gòu)中并沒(méi)有和數(shù)據(jù)庫(kù)連接相關(guān)的字段,那gorm.Open到底是如何和mysql數(shù)據(jù)庫(kù)建立連接的呢? 我們繼續(xù)深入gorm.Open函數(shù)和mysql.Open函數(shù)的詳細(xì)內(nèi)容。
二、gorm.Open函數(shù)
在gorm.Open函數(shù)中,傳入的參數(shù)是一個(gè)Dialector接口類型的dialector變量。我們看到會(huì)將傳入的Dialector變量賦值給配置config.Dialector,如下:
config.Dialector = dialector
然后,又通過(guò)config.Dialector的Initialize函數(shù)對(duì)數(shù)據(jù)庫(kù)進(jìn)行了初始化。如下:
err = config.Dialector.Initialize(db)
那么,Dialector是什么呢?Dialector是通過(guò)gorm.Open函數(shù)的第一個(gè)參數(shù)傳進(jìn)來(lái)的。我們看具體的是什么。
三、Dialector參數(shù)
在gorm.Open函數(shù)中,第一個(gè)參數(shù)是Dialector類型的參數(shù),這是一個(gè)接口類型。也就是說(shuō)只要實(shí)現(xiàn)了該接口,就能作為一個(gè)Dialector。這也就是gorm能夠針對(duì)很多數(shù)據(jù)庫(kù)進(jìn)行操作的原因。比如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ù)庫(kù),我們看到是通過(guò)gorm.io/driver/mysql庫(kù)的Open函數(shù)來(lái)初始化的。我們看下mysql.Open函數(shù)的實(shí)現(xiàn),如下:
func Open(dsn string) gorm.Dialector { dsnConf, _ := mysql.ParseDSN(dsn) return &Dialector{Config: &Config{DSN: dsn, DSNConfig: dsnConf}} }
該函數(shù)接收一個(gè)dsn的字符串,也就是第一節(jié)中我們提供的和數(shù)據(jù)庫(kù)相關(guān)的賬號(hào)密碼等連接數(shù)據(jù)的信息。然后,返回的是mysql驅(qū)動(dòng)包中的Dialector對(duì)象。該對(duì)象包含了相關(guān)的配置。
然后,是在gorm.Open函數(shù)中,調(diào)用了Dialector的Initialize函數(shù)。我們看下該函數(shù)中和數(shù)據(jù)庫(kù)連接相關(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行的地方,是通過(guò)sql.Open函數(shù)來(lái)進(jìn)行具體的和數(shù)據(jù)庫(kù)進(jìn)行連接的。然后返回的對(duì)象是sql.DB類型,大家注意,這里的sql.DB類型是go標(biāo)準(zhǔn)庫(kù)中的DB,而非gorm庫(kù)中的DB。返回的sql.DB對(duì)象賦值給了gorm中DB對(duì)象中的ConnPool。
同時(shí),在gorm.Open函數(shù)中,還將db.ConnPool對(duì)象賦值給了db.Statement.ConnPool對(duì)象。到這里是不是gorm.DB結(jié)構(gòu)體中的字段就和數(shù)據(jù)庫(kù)的具體連接關(guān)聯(lián)起來(lái)。
接下來(lái),我們?cè)倏纯磗ql.Open函數(shù)是如何和數(shù)據(jù)庫(kù)建立連接的。
四、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 }
我們先簡(jiǎn)單分析下上述代碼。在第3行處,從drivers中獲取對(duì)應(yīng)的驅(qū)動(dòng)名稱的具體驅(qū)動(dòng)對(duì)象。這里的driverName是mysql。然后從第9行到第14行是執(zhí)行具體驅(qū)動(dòng)程序的連接函數(shù)。
首先,我們先看從drivers中根據(jù)驅(qū)動(dòng)名稱mysql獲取驅(qū)動(dòng)對(duì)象的邏輯。 drivers是標(biāo)準(zhǔn)庫(kù)sql中的一個(gè)map類型,如下:
drivers = make(map[string]driver.Driver)
該變量是通過(guò)sql包中的Register函數(shù)進(jìn)行注冊(cè)的:
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ù)又是在哪里進(jìn)行調(diào)用的呢?我們?cè)倩卣{(diào)gorm.Open函數(shù)中,第一個(gè)參數(shù)調(diào)用的是mysql.Open函數(shù)。也就是說(shuō)引入了庫(kù)gorm.io/driver/mysql,在該庫(kù)中,我們看到又引入了github.com/go-sql-driver/mysql庫(kù)。該庫(kù)中有一個(gè)init方法,如下:
func init() { sql.Register("mysql", &MySQLDriver{}) }
原來(lái),這里調(diào)用了標(biāo)準(zhǔn)庫(kù)sql中的Register函數(shù),將“mysql”和對(duì)應(yīng)的驅(qū)動(dòng)對(duì)象MySQLDriver進(jìn)行了注冊(cè)關(guān)聯(lián)。
我們?cè)俜祷貋?lái)看sql.Open函數(shù)的具體實(shí)現(xiàn)。那這里就繼續(xù)調(diào)用MySQLDriver的OpenConnector方法。我們看下該方法的實(shí)現(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ù)首先通過(guò)ParseDSN解析dsn字符串中的用戶名,地址,密碼等配置選項(xiàng)。然后返回一個(gè)connector對(duì)象。該connector對(duì)象就是在sql.Open函數(shù)中執(zhí)行的OpenDB(connector)函數(shù)中的參數(shù)。
我們繼續(xù)看sql.OpenDB函數(shù)的實(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)建了一個(gè)sql.DB對(duì)象,同時(shí)執(zhí)行了一個(gè)協(xié)程進(jìn)行數(shù)據(jù)庫(kù)的連接:
go db.connectionOpener(ctx)
接著看db.connectionOpener函數(shù)的實(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) } } }
這里,有一個(gè)db.openNewConnection函數(shù),根據(jù)名字可知是打開新的連接。其實(shí)現(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() } }
這里我們看到有一個(gè)db.connector.Connect函數(shù),connector就是github.com/go-sql-driver/mysql庫(kù)中的connector對(duì)象。我們回到該庫(kù),查看其Connect函數(shù)的實(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行,這里進(jìn)行了實(shí)際的撥號(hào)操作,也就是和數(shù)據(jù)庫(kù)真正的建立了連接。再看第27行,斷言是一個(gè)TCP連接。第37行,進(jìn)行了握手處理;第45行,進(jìn)行了認(rèn)證處理。最終返回了一個(gè)mysqlConn對(duì)象。該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ù)庫(kù)建立的TCP的連接。
五、從mysql到gorm.DB
我們?cè)倏偨Y(jié)下上述和mysql相關(guān)的各個(gè)對(duì)象之間的關(guān)聯(lián)關(guān)系。從mysql開始逆向推導(dǎo)。如下:
也就是說(shuō),我們?cè)谑褂胓orm進(jìn)行數(shù)據(jù)庫(kù)操作的時(shí)候,最終都是從gorm.Statement.ConnPool中獲取的數(shù)據(jù)庫(kù)連接來(lái)具體執(zhí)行sql語(yǔ)句的。
到此這篇關(guān)于深入理解gorm如何和數(shù)據(jù)庫(kù)建立連接的文章就介紹到這了,更多相關(guān)gorm連接數(shù)據(jù)庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang使用bcrypt實(shí)現(xiàn)密碼加密和校驗(yàn)的操作代碼
bcrypt可以用于數(shù)據(jù)庫(kù)中的用戶密碼保存,相比md5而言更加的安全可靠,這篇文章主要介紹了Golang使用bcrypt實(shí)現(xiàn)密碼加密和校驗(yàn)的操作代碼,需要的朋友可以參考下2024-05-05Go type關(guān)鍵字(類型定義與類型別名的使用差異)用法實(shí)例探究
這篇文章主要為大家介紹了Go type關(guān)鍵字(類型定義與類型別名的使用差異)用法實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Go導(dǎo)入不同目錄下包報(bào)錯(cuò)的解決方法
包(package)是多個(gè)Go源碼的集合,是一種高級(jí)的代碼復(fù)用方案,下面這篇文章主要給大家介紹了關(guān)于Go導(dǎo)入不同目錄下包報(bào)錯(cuò)的解決方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06Golang微服務(wù)框架Kratos實(shí)現(xiàn)分布式任務(wù)隊(duì)列Asynq的方法詳解
任務(wù)隊(duì)列(Task Queue) 一般用于跨線程或跨計(jì)算機(jī)分配工作的一種機(jī)制,在Golang語(yǔ)言里面,我們有像Asynq和Machinery這樣的類似于Celery的分布式任務(wù)隊(duì)列,本文就給大家詳細(xì)介紹一下Golang微服務(wù)框架Kratos實(shí)現(xiàn)分布式任務(wù)隊(duì)列Asynq的方法,需要的朋友可以參考下2023-09-09使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存的方法
這篇文章主要介紹了使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存,本文比較了字節(jié)緩存和結(jié)構(gòu)體緩存的優(yōu)劣勢(shì),介紹了緩存穿透、緩存錯(cuò)誤、緩存預(yù)熱、緩存?zhèn)鬏?、故障轉(zhuǎn)移、緩存淘汰等問(wèn)題,并對(duì)一些常見(jiàn)的緩存庫(kù)進(jìn)行了基準(zhǔn)測(cè)試,需要的朋友可以參考下2022-05-05