01.理解database/sql|青训营笔记

97 阅读3分钟

01.理解database/sql

database/sql的基本用法,设计原理,基础概念介绍

1.1基本用法- Quick Start

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	//使用driver+DSN初始化DB连接
	//这里DSN(第二个参数)的格式是
	//用户名:密码@tcp(127.0.0.1:3306)/数据库名,其中3306是默认端口
	db, err := sql.Open("mysql", "root:******@tcp(127.0.0.1:3306)/hello")

	// 执行一条sql,意思是选择users中id=1的记录的id和名字
	//driver会将这句sql发送给数据库,通过rows取回返回的数据

	rows, err := db.Query("select id,name from users where id= ?", 1)

	if err != nil {
		//...
	}
	//处理完毕后记得释放连接
	defer func() {
		err = rows.Close()
	}()

	//数据、错误处理
	var users []User
	//rows是一个游标,Next()不断获取下一条数据
	for rows.Next() {
		//User应该是一个结构体
		var user User
		//将rows中的数据扫描到user中
		err := rows.Scan(&user.ID, &user.Name)

		if err != nil {
			//....
		}
		//将user添加到users列表中
		users = append(users, user)
	}
	//测试一下输出
	fmt.Println(users)

	if rows.Err() != nil {
		//....
	}
}

type User struct {
	ID   int64
	Name string
}

作者:天亦哥哥
链接:<https://juejin.cn/post/7098358444519866381>
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1.2 设计原理

Untitled

对于不同的数据库只要改变接口就行,连接池为池化技术

池化技术:把一些能够复用的东西(比如说数据库连接、线程)放到池中,避免重复创建、销毁的开销,从而极大提高性能。 在开发过程中我们会用到很多的连接池,像是数据库连接池、HTTP 连接池、 Redis 连接池等等。

连接池管理

连接池配置

连接池状态

database实际的操作过程

//maxBadConnRetries默认为2
	for i:=0;i<maxBadConnRetries;i++{
	//从连接池获取连接或通过driver新建连接	
		dc,err:=db.Conn(ctx,strategy)
		//**获取连接的两种策略**
		//有空闲连接 -> reuse -> max life time
		//若无空闲连接,新建连接 -> max open...

		//将连接放回连接池
		defer dc.db.putConn(dc,err,true)
		//校验
		//validateConnection 有无错误
		//max life time,max idle conns 检查
		
		//连接实现一些操作的接口 driver.Queryer,driver.Execer等interface
		if err==nil{
			err=dc.ci.Query(sql,args...)
		}
		//如果返回BadConn的错误,就回重新for循环两次,如果任意有一次没有错误,就会break出来
		isBadConn=errors.Is(err,driver.ErrBadConn)
		if !isBadConn{
			break
		}
	}
//业务代码
import _ "github.com/go-sql-driver/mysql"

func main(){
	db,err := sql.Open("mysql","grom:grom@tcp(localhost:9910)/grom?charset=utf&&parseTime=True&loc=Local")
}

//注册driver
func init(){
//调用Register方法
	sql.Register("mysql",&MySQLDriver{})
}

maxBadConnRetries是一个固定值,可能会造成重复插入的状态

Driver连接接口

//Driver接口
type Driver interface{
	//Open 返回一个数据库的新的连接
	Open(name string)(Conn,error)
}

//注册全局driver
func Register(name string,driver driver.Driver){
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil{
		panic("sql:Register driver is nil")
	}
	//如果这个名字的driver已经有了的话,报错
	if _,dup := drivers[name];dup{
		panic("sql : Register called twice for driver "+name)
	}
	//drivers应该是个全局的map,我们把driver放到全局变量中
	drivers[name]=driver
}

之前的用法容易出现import顺序导致的错误,所以提出将DSN转为结构体的方式

Driver连接接口2

这样创建一个链接,会有一个强制编译的检查,不会忘记import

type Connector interface{
	Connect(context.Context)(Conn,error)
	Driver() Driver
}

func OpenDB(c driver.Connector) *DB{
	// ...
}
import "github.com/go-sql-driver/mysql"
func main(){
	connector,err := mysql.NewConnector(&mysql.Config{
		User:		"gorm",
		Passwd:		"gorm",
		Net:		"tcp",
		Addr:		"127.0.0.1:3306",
		DBName:		"gorm",
		ParseTime:	"true",
	})
	
	db:=sql.OpenDB(connector)
}

操作接口

DB连接的几种类型 直接连接/ Conn:简单的tcp连接 预编译/ Stmt:先生成一个prepare statement以及其reference ID,后面执行同样的sql时,就不需要传递原来的sql,只需要找一下reference ID,减少网络传输和数据库解析的时间 事务/ Tx

处理返回数据的几种方式:

  • Exec/ExecContext -> result:执行sql只关心结果是否成功
  • Query/QueryContext -> Rows(Columns)查询以行(列)的形式放回
  • QueryRow/QueryRowContext -> Row(Rows的简化),只会读一行的数据,一行读了之后自动close
type driver.Rows interface{
	//返回columns名字
	Columns() []string
	
	//实现数据库协议
	//解析数据库到database/sql.Rows.lastcols中
	Next(dest []Value) error
	
	//多批数据解析
	HasNextResultSet() bool
	NextResultSet() error
}
type Rows struct{
	dc		*driverConn
	lastcols	[]driver.Value
	//...
}

func (rs /Rows) Scan(dest...any) error {
	for i,sv:=range rs.lastcols {
		err := convertAssignRows(dest[i],sv,rs)
		if err!=nil{
			return fmt.Errorf(`sql:Scan error on column index %d , name %q:%w`,i,rs.rowsi.Columns()[i],err)
		}
	}
	return nil
}

func convertAssignRows(dest,src any,rows *Rows) error{
	//...常见的几种数据类型的赋值
}