深入理解gorm如何和数据库建立连接

 更新时间:2023年11月06日 11:22:44   作者:Go学堂  
这篇文章主要为大家详细介绍了gorm如何和数据库建立连接,文中的示例代码讲解详细,对我们深入了解GO语言有一定的帮助,需要的小伙伴可以参考下

一、gorm.Open

通常情况下,我们是通过gorm.Open函数就能在应用层和数据建立连接。如下:

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

在该代码片段中,我们传入了数据库的用户名、密码、地址以及数据库和数据库对应的配置。然后通过gorm.Open函数就和数据库建立连接了,gorm.Open函数返回的是一个gorm.DB对象。如下:

// DB GORM DB definition
type DB struct {
	*Config
	Error        error
	RowsAffected int64
	Statement    *Statement
	clone        int
}

在该数据结构中并没有和数据库连接相关的字段,那gorm.Open到底是如何和mysql数据库建立连接的呢? 我们继续深入gorm.Open函数和mysql.Open函数的详细内容。

二、gorm.Open函数

在gorm.Open函数中,传入的参数是一个Dialector接口类型的dialector变量。我们看到会将传入的Dialector变量赋值给配置config.Dialector,如下:

config.Dialector = dialector

然后,又通过config.Dialector的Initialize函数对数据库进行了初始化。如下:

err = config.Dialector.Initialize(db)

那么,Dialector是什么呢?Dialector是通过gorm.Open函数的第一个参数传进来的。我们看具体的是什么。

三、Dialector参数

在gorm.Open函数中,第一个参数是Dialector类型的参数,这是一个接口类型。也就是说只要实现了该接口,就能作为一个Dialector。这也就是gorm能够针对很多数据库进行操作的原因。比如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的数据库,我们看到是通过gorm.io/driver/mysql库的Open函数来初始化的。我们看下mysql.Open函数的实现,如下:

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

该函数接收一个dsn的字符串,也就是第一节中我们提供的和数据库相关的账号密码等连接数据的信息。然后,返回的是mysql驱动包中的Dialector对象。该对象包含了相关的配置。

然后,是在gorm.Open函数中,调用了Dialector的Initialize函数。我们看下该函数中和数据库连接相关的逻辑。

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函数来进行具体的和数据库进行连接的。然后返回的对象是sql.DB类型,大家注意,这里的sql.DB类型是go标准库中的DB,而非gorm库中的DB。返回的sql.DB对象赋值给了gorm中DB对象中的ConnPool。

同时,在gorm.Open函数中,还将db.ConnPool对象赋值给了db.Statement.ConnPool对象。到这里是不是gorm.DB结构体中的字段就和数据库的具体连接关联起来。

接下来,我们再看看sql.Open函数是如何和数据库建立连接的。

四、sql.Open函数

先看sql.Open函数的源代码:

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中获取对应的驱动名称的具体驱动对象。这里的driverName是mysql。然后从第9行到第14行是执行具体驱动程序的连接函数。

首先,我们先看从drivers中根据驱动名称mysql获取驱动对象的逻辑。 drivers是标准库sql中的一个map类型,如下:

drivers   = make(map[string]driver.Driver)

该变量是通过sql包中的Register函数进行注册的:

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
}

该函数又是在哪里进行调用的呢?我们再回调gorm.Open函数中,第一个参数调用的是mysql.Open函数。也就是说引入了库gorm.io/driver/mysql,在该库中,我们看到又引入了github.com/go-sql-driver/mysql库。该库中有一个init方法,如下:

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

原来,这里调用了标准库sql中的Register函数,将“mysql”和对应的驱动对象MySQLDriver进行了注册关联。

我们再返回来看sql.Open函数的具体实现。那这里就继续调用MySQLDriver的OpenConnector方法。我们看下该方法的实现如下:

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

该函数首先通过ParseDSN解析dsn字符串中的用户名,地址,密码等配置选项。然后返回一个connector对象。该connector对象就是在sql.Open函数中执行的OpenDB(connector)函数中的参数。

我们继续看sql.OpenDB函数的实现,如下:

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
}

这里首先构建了一个sql.DB对象,同时执行了一个协程进行数据库的连接:

go db.connectionOpener(ctx)

接着看db.connectionOpener函数的实现,如下:

// 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函数,根据名字可知是打开新的连接。其实现如下:

// 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函数,connector就是github.com/go-sql-driver/mysql库中的connector对象。我们回到该库,查看其Connect函数的实现:

// 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行,这里进行了实际的拨号操作,也就是和数据库真正的建立了连接。再看第27行,断言是一个TCP连接。第37行,进行了握手处理;第45行,进行了认证处理。最终返回了一个mysqlConn对象。该mysqlConn结构体中包含字段如下:

type mysqlConn struct {
	buf              buffer
	netConn          net.Conn
	rawConn          net.Conn // underlying connection when netConn is TLS connection.
    // ...
}

其中,netConn就是和数据库建立的TCP的连接。

五、从mysql到gorm.DB

我们再总结下上述和mysql相关的各个对象之间的关联关系。从mysql开始逆向推导。如下:

也就是说,我们在使用gorm进行数据库操作的时候,最终都是从gorm.Statement.ConnPool中获取的数据库连接来具体执行sql语句的。

到此这篇关于深入理解gorm如何和数据库建立连接的文章就介绍到这了,更多相关gorm连接数据库内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang连接PostgreSQL基本操作的实现

    Golang连接PostgreSQL基本操作的实现

    PostgreSQL是常见的免费的大型关系型数据库,本文主要介绍了Golang连接PostgreSQL基本操作的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • golang对自定义类型进行排序的解决方法

    golang对自定义类型进行排序的解决方法

    学习一门编程语言,要掌握原子数据类型,还需要掌握自定义数据类型。下面这篇文章主要给大家介绍了关于golang如何对自定义类型进行排序的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下。
    2017-12-12
  • 详解Go语言中关于包导入必学的 8 个知识点

    详解Go语言中关于包导入必学的 8 个知识点

    这篇文章主要介绍了详解Go语言中关于包导入必学的 8 个知识点,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • 关于golang中map使用的几点注意事项总结(强烈推荐!)

    关于golang中map使用的几点注意事项总结(强烈推荐!)

    map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用,下面这篇文章主要给大家介绍了关于golang中map使用的几点注意事项,需要的朋友可以参考下
    2023-01-01
  • 解决golang gin框架跨域及注解的问题

    解决golang gin框架跨域及注解的问题

    这篇文章主要介绍了解决golang gin框架跨域及注解的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • GO语言并发之好用的sync包详解

    GO语言并发之好用的sync包详解

    标准库中的sync包在我们的日常开发中用的颇为广泛,那么大家对sync包的用法知道多少呢,这篇文章就大致讲一下sync包和它的使用,感兴趣的可以学习一下
    2022-12-12
  • Go实现自己的网络流量解析和行为检测引擎原理

    Go实现自己的网络流量解析和行为检测引擎原理

    这篇文章主要为大家介绍了Go实现自己的网络流量解析和行为检测引擎原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Golang上下文Context的常见应用场景

    Golang上下文Context的常见应用场景

    Golang context主要用于定义超时取消,取消后续操作,在不同操作中传递值。本文通过简单易懂的示例进行说明,感兴趣的可以了解一下
    2023-04-04
  • golang http使用踩过的坑与填坑指南

    golang http使用踩过的坑与填坑指南

    这篇文章主要介绍了golang http使用踩过的坑与填坑指南,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go语言中你所不知道的位操作用法

    Go语言中你所不知道的位操作用法

    位运算可能在平常的编程中使用的并不多,但涉及到底层优化,一些算法及源码可能会经常遇见。下面这篇文章主要给大家介绍了关于Go语言中你所不知道的位操作用法的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下。
    2017-12-12

最新评论