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 设计原理
对于不同的数据库只要改变接口就行,连接池为池化技术
池化技术:把一些能够复用的东西(比如说数据库连接、线程)放到池中,避免重复创建、销毁的开销,从而极大提高性能。 在开发过程中我们会用到很多的连接池,像是数据库连接池、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{
//...常见的几种数据类型的赋值
}