Go原生sql会用吗?database/sql

是什么

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&...&paramN=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 会占用内存,在循环中需要关闭的话可以显式调用close
  • rows.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以及影响的记录数
  • 不同于之前的QueryExec执行完后会立刻释放连接,不会占用资源
  • 不要用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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值