是什么
Go 官方提供了 database/sql
包来给用户进行和数据库打交道的工作,database/sql
库实际只提供了一套操作数据库的接口和规范,例如抽象好的 SQL 预处理(prepare),连接池管理,数据绑定,事务,错误处理等等。
分析
database/sql
官方并没有提供具体某种数据库实现的协议支持。
使用的时候需要根据实际的数据库,引入对应的驱动
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@/dbname")
可以看看引入驱动的init
干了啥
// driver.go
func init() {
sql.Register("mysql", &MySQLDriver{})
}
// /database/sql/sql.go
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
}
// /database/sql/driver/driver.go
type Driver interface {
Open(name string) (Conn, error)
}
可以看到主要是把驱动注册到drivers这个map中
Register
入参中的driver.Driver
是一个接口,只要实现它的Open方法即可
继续找找MySQLDriver
的Open方法干了啥
// Open new Connection.
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
// the DSN string is formatted
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
c := &connector{
cfg: cfg,
}
return c.Connect(context.Background())
}
可以看到,通过这个函数来创建一个连接,Open函数接收一个需要连接的数据库参数DSN,从文档可以找到DSN的标准格式
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
Open的返回driver.Conn
,也是一个接口;如果你仔细地查看 database/sql/driver/driver.go
的代码会发现,这个文件里所有的成员全都是接口,对这些类型进行操作,还是会调用具体的 driver
里的方法
// 不支持并发 conn有状态
type Conn interface {
// 准备一个 SQL 语句,并将其绑定到当前连接
Prepare(query string) (Stmt, error)
// 释放不需要的连接
Close() error
// 事务
Begin() (Tx, error)
}
用法
创建
func main() {
db, err := sql.Open("mysql",
"user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
// defer db.Close() 长生命周期 复用一个对象
}
- 长生命周期:
sql.DB
对象不应该被频繁打开和关闭。 - 连接池:
sql.DB
内部维护了一个连接池,可以高效地重用和共享连接,从而提高性能和资源利用率。
不过,Open并不立即和数据库建立连接,只有在第一次需要使用的时候才会真正和数据库建立连接;如果要测试和数据库的连接 可以使用db.Ping()
基本查询
# 查询users表中id=1的记录,只用返回id和name
select id, name from users where id = 1
返回多条记录
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
db.Query()
向数据库发送查询rows.Close()
关闭连接;可以重复调用;用defer 会占用内存,在循环中需要关闭的话可以显式调用closerows.Next()
遍历查询记录;在迭代中也要加上错误处理;rows.Scan()
将记录返回的字段值 扫描到对应变量;这里需要自己来保证接收变量的类型正确,而且需要用指针来接收
rows只要没被close掉,那它就会占用连接池里的连接,因此使用完一定要记得关闭
rows.Scan的工作原理
Go会自动帮你做好数据类型转化,自动转成目标数据类型
比如数据库里存的是 字符串类型 VARCHAR
但实际存的可能是数字,这时只需要传入一个int类型的指针,Go就会自动做好转换, 转换有错误也会自动抛出,不需要手动去调用strconv.ParseInt()
这类的方法自己做转换
准备查询
反复用到的查询语句应该要提前准备好
准备查询返回一个预编译语句,这个预编译是指数据库预先编译好了这个查询并存储执行计划,这样在实际执行查询的时候,速度快一些
可变参数用占位符替代
以mysql
为例
// 准备查询
stmt, err := db.Prepare("select id, name from users where id = ?")
if err != nil {...}
defer stmt.Close() // 必须关闭
// 执行查询
rows, err := stmt.Query(1)
if err != nil {...}
defer rows.Close()
for rows.Next() {...} // same like before
err = rows.Err()
if err != nil {...}
原来的db.Query()
工作流程
- 准备:调用db.Prepare()
- 执行:调用stmt.Query()
- 关闭:关闭预编译语句,释放数据库资源
每次查询都需要重复这个三个流程,要和数据库进行三次交互,因此如果要重复执行某个查询语句,可以用准备查询来优化查询流程,减少和数据库的交互次数
准备查询工作原理
在数据库层面,一个准备查询是和数据库的一个连接绑定的
在database/sql
中,连接不会暴露给用户,准备查询是用一个DB或者TX这类的对象来发起,由包来做准备查询和连接的关联,实际上包会在连接池里选个连接和准备查询关联
准备查询返回的Stmt
对象会记住它是在哪个连接上准备的,因此后续查询需要调用stmt.Query
,如果stmt
对象发现它记住的连接不可用,自己会去连接池上捞另一个连接,并且重新准备语句
潜在问题
- 语句泄漏(Statement Leak):预编译语句没有被正确关闭,在内存积累
- 高并发时,大多连接都繁忙,这可能会让预编译语句不断重新准备
- 性能问题,频繁准备意味着和数据库交互也很频繁,网络和数据库负担重
返回单条记录
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
结合准备查询
stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {...}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {...}
让我们来看看 QueryRow
和 之前用到的Query
实现上有什么区别
func (db *DB) QueryRow(query string, args ...any) *Row {
return db.QueryRowContext(context.Background(), query, args...)
}
func (db *DB) Query(query string, args ...any) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
type Row struct {
// One of these two will be non-nil:
err error
rows *Rows
}
可以看到,QueryRow
内部调用的方法和之前提到的Query是一致的,只是返回类型不同, 甚至Row类型只是在Rows的基础上多封装了一个error
那为什么一个返回单条记录一个返回多条记录呢?答案就在Row的Scan方法中
func (r *Row) Scan(dest ...any) error {
...
defer r.rows.Close()
...
if !r.rows.Next() {
if err := r.rows.Err(); err != nil {
return err
}
return ErrNoRows
}
err := r.rows.Scan(dest...)
...
// Make sure the query can be processed to completion with no errors.
return r.rows.Close()
}
是熟悉的r.rows.Next()
操作,它会从查询结果第一行开始移动,回忆之前查询多条记录时的循环迭代操作,这里只调用了一次,因此移动到第一个结果就结束,随后调用Scan进行扫描,如果一条匹配记录都没有,那么会返回ErrNoRows
修改数据
用Exec
来实现 增删改 操作
通常搭配 预查询语句
stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
Exec
返回sql.Result
对象, 可以通过res拿到最后插入的ID以及影响的记录数- 不同于之前的
Query
,Exec
执行完后会立刻释放连接,不会占用资源 - 不要用
Query
来执行增删改
事务
保证所有操作都在一个连接中完成
tx, err := db.Begin() // 开始事务
if err != nil {
log.Fatal(err)
}
// sql操作
_, err = tx.Exec("DELETE FROM users WHERE id = ?", 1)
if err != nil {
tx.Rollback() // 回滚事务
log.Fatal(err)
}
_, err = tx.Exec("UPDATE orders SET status = 'cancelled' WHERE user_id = ?", 1)
if err != nil {
tx.Rollback() // 回滚事务
log.Fatal(err)
}
err = tx.Commit() // 提交事务
if err != nil {
tx.Rollback() // 回滚事务
log.Fatal(err)
}
- 在开始事务之后,后续所有操作都应该通过事务创建的
Tx
对象来调用
搭配 准备查询
tx, err := db.Begin()
if err != nil {...}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {...}
defer stmt.Close()
for i := 0; i < 10; i++ {
_, err = stmt.Exec(i)
if err != nil {...}
}
err = tx.Commit()
if err != nil {...}
Null处理
如果数据库字段可为null 则需要额外处理逻辑
database/sql
包中提供了部分的nullable types
for rows.Next() {
var s sql.NullString
err := rows.Scan(&s)
// check err
if s.Valid {
// use s.String
} else {
// NULL value
}
}
但如果需要用到其他的nullable types 就需要自己定义
如果不想用nullable types来处理NULL 可以使用数据库层面的COALESCE()函数来避免抛出null
rows, err := db.Query(`
SELECT
name,
COALESCE(other_field, '') as otherField
WHERE id = ?
`, 42)
for rows.Next() {
err := rows.Scan(&name, &otherField)
// ..
// If `other_field` was NULL, `otherField` is now an empty string. This works with other data types as well.
}
处理返回的列
rows.Columns()
返回列名切片
cols, err := rows.Columns()
if err != nil {
// handle the error
} else {
dest := []interface{}{ // Standard MySQL columns
new(uint64), // id
new(string), // host
new(string), // user
new(string), // db
new(string), // command
new(uint32), // time
new(string), // state
new(string), // info
}
if len(cols) == 11 {
// Percona Server
} else if len(cols) > 8 {
// Handle this case
}
err = rows.Scan(dest...)
// Work with the values in dest
}
如果不知道一些列的类型,可以用sql.RawBytes
来接收
cols, err := rows.Columns()
if err != nil {...}
// 创建值切片
vals := make([]interface{}, len(cols))
for i := range cols {
vals[i] = new(sql.RawBytes)
}
for rows.Next() {
err = rows.Scan(vals...)
if err != nil {...}
// 处理每一行的数据
for i, col := range cols {
raw := vals[i].(*sql.RawBytes)
if raw == nil {
fmt.Printf("%s: NULL\n", col)
} else {
// 将 raw 转换为具体类型
...
fmt.Printf("%s: %v\n", col, value)
}
}
}
ORM
原生sql不足
查询结果要手动把rows映射到对应的类型或结构体里,如果你使用了 select *
这样的查询,你需要为每一个返回字段都定义好接收的变量,非常繁琐
如果数据库表结构能映射到Go结构体 能省去很多手动映射工作
ORM
对象关系映射(Object Relational Mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。 从效果上说,它其实是创建了一个可在编程语言里使用的 “虚拟对象数据库”。
和gorm对比一下
原生sql
type User struct {
ID int
Name string
}
type Order struct {
ID int
UserID int
Amount float64
}
func main() {
// 数据库连接
...
// 查询用户
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
log.Fatal(err)
}
users = append(users, user)
}
// 查询订单
rows, err = db.Query("SELECT id, user_id, amount FROM orders")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var orders []Order
for rows.Next() {
var order Order
if err := rows.Scan(&order.ID, &order.UserID, &order.Amount); err != nil {
log.Fatal(err)
}
orders = append(orders, order)
}
}
gorm
type User struct {
ID int
Name string
Orders []Order `gorm:"foreignkey:UserID"`
}
type Order struct {
ID int
UserID int
Amount float64
}
func main() {
// 数据库连接
...
// 自动迁移表结构
db.AutoMigrate(&User{}, &Order{})
// 查询用户及其订单
var users []User
db.Preload("Orders").Find(&users)
// 结果处理
...
}
当然,使用ORM库能让开发变得简洁高效,但它也有它的代价,首先性能上肯定不如原生sql,毕竟中间包装了一层,其次是,ORM库可能会屏蔽掉很多细节,稍不注意用法可能就会拉爆数据库
扩展
可以参考官方文档
http://go-database-sql.org/