七叶笔记 » golang编程 » 大白话 golang 教程-22-关系型数据库访问

大白话 golang 教程-22-关系型数据库访问

go 原生提供了对数据库的支持,就是 database/sql 包,对关系型的数据库进行了通用的抽象,轻量、面向行的接口,所以使用这个包还需要下载对相应的数据库驱动,比如 mysql 的驱动包 github.com/go-sql-driver/mysql,执行:

 go get -u github.com/go-sql-driver/mysql  

由于不需要调用驱动包的含糊,只需要其执行一次 init 函数,imoprt 部分往往是:

 "database/sql"
_ "github.com/go-sql-driver/mysql"  

不引入驱动,调用 sql 包的函数的时候会收到提示:

 sql: unknown driver "mysql" (forgotten import?)  

为什么引入 mysql 的包后就可以工作了呢? 这是因为 mysql 包里的 init 函数里执行了 sql.Register 函数:

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

查看 Register 函数的实现,会发现内部把驱动类型和驱动实例做了一个映射保存在 drivers 中,对 MySQLDriver 来说,就是 “mysql” => &MySQLDriver{},Register 函数的第二个参数是 driver.Driver 接口对象,它只要求实现 1 个方法:

 type Driver interface { 
  Open(name string) (Conn, error)
}  

Open 函数返回一个 driver.Conn 对象,根据注释得知该 Conn 对象在同一时间只会被一个 goroutine 所占用,通过查看 MySQLDriver 的 Open 函数发现它使用了内部的 connector 对象来实现,而 connector 对象又将功能更委托给了 mysqlConn 对象,该连接对象负责和 mysql 之间交互的协议。

sql 包使用 sql.Open 来获得一个数据库操作对象 sql.DB,而这个 DB 结构体中最重要的就是 driver.Connector,它要求实现 2 个方法:

1. Connect(context.Context) (Conn, error)

2. Driver() Driver

 func getMysqlDB() *sql.DB { 
  db, _ := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=true") 
  //db, _ := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True") 
  //db.Ping() 
  return db
}  

问题是这个 sql.Open 里的 connector 是怎么和 mysql 驱动的 connector 连接上的呢? F12 进入 sql.Open 函数,发现它会尝试从 dirvers 获取 MySQLDriver 的实例,尝试转换成 driver.DriverContext 后调用其 OpenConnector 函数,如果转换失败了也会构造一个内部的 dsnconnector 对象,调用 driver 上的 Open 方法。

而 MySQLDriver 对 Open 和 OpenConnector 都提供了实现:

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

func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
  cfg, err := ParseDSN(dsn)
  if err != nil {
    return nil, err
  }
  return &connector{
    cfg: cfg,
  }, nil
}  

sql 包依靠 Open、Register 2 个函数实现了驱动包的注入,获得 driver.Conn 对象后,主要工作就是解析 sql 查询和获得结果了(mysql 交互协议),因为查询通常分为 2 种类型:

1. query 查询带结果,比如 select 等

2. exec 只需执行,比如 update、delete 等

具体 sql 包在调用时会尝试把 driver.Conn 对象转换成 Queryer、QueryerContext 和 Execer、ExecerContext 执行其对应的方法,不过这一切使用 sql.DB 做为中间对象来操作,在 DB 内部它对 driver.Conn 对象做了进一层带锁的包装 driverConn,在任何查询前都会使用 db.conn 内部方法里获取 driverConn,它加锁以后从 freeConn 里取出一个并标记为 inUse 在使用,否则就检测是否达到最大连接数,没有就就调用 connector 对象的 Connect 方法获得 driver.Conn 后构造一个新的 driverConn,用完了后都会调用它的 relaseConn 把连接对象重新 put 回 freeConn 列表里。至此,go sql 驱动包的加载和执行的流程都清楚了。

mysql 的 go hello 版本如下:

 func main() { 
  var str string db := getMysqlDB() 
  row := db.QueryRow("SELECT 'hello go-mysql'") 
  if row.Scan(&str) == nil {  
    fmt.Println(str) 
  }
}  

批量查询和使用预编译的查询:

 func scan() {
  db := session.GetMysqlDB()
  rows, _ := db.Query("SELECT * FROM user")
  defer rows.Close()
  // 遍历 rows
}

func single() {
  var user User
  db := session.GetMysqlDB()
  db.QueryRow("SELECT * FROM user WHERE id = 1").Scan(&user.id, &user.passport, &user.password, &user.nickname, &user.createdAt)
  fmt.Println(user)
}

func prepare() {
  db := session.GetMysqlDB()
  stmt, _ := db.Prepare("SELECT * FROM user WHERE id = ?")
  defer stmt.Close()
  rows, _ := stmt.Query(1)
  defer rows.Close()
  //
}  

怎么遍历 rows 呢? 需要使用 Next() 来判断,当遇到 io.EOF 或者 rows 被 close 会结束遍历。

 for rows.Next() { 
  var user User 
  rows.Scan(&user.id, &user.passport, &user.password, &user.nickname, &user.createdAt) 
  fmt.Println(user)
}

// 使用 rows.Error 来得到错误  

插入时使用 LastInsertId 来获取最后一行的 id:

 func main() {
  db := session.GetMysqlDB()
  stmt, _ := db.Prepare("INSERT INTO user(`passport`, `password`,`nickname`) VALUES(?, ?, ?)")
  result, _ := stmt.Exec("lisi", "abc123", "李四")
  fmt.Println(result.LastInsertId())
}  

使用 begin 和 beginTx 开启一个事务,begin 使用默认的 context.Background() 来调用 beginTx,事务会独占数据库连接,但是 Tx 对象上的方法和 sql.DB 都是一一对应,调用 commit 或者 rollback 后,Tx 对象才被 Close 掉。

 func main() {
  db := session.GetMysqlDB()
  stmt, _ := db.Prepare("INSERT INTO user(`passport`, `password`,`nickname`) VALUES(?, ?, ?)")
  result, _ := stmt.Exec("lisi", "abc123", "李四")
  fmt.Println(result.LastInsertId())
}  

NULL 字段有时候会很麻烦,处理 NULL 有两个办法:

1. 使用数据库函数 COALESCE 让可能 NULL 的字段不可能返回 NULL

2. 使用 sql.NullString、sql.NullInt32 等类型

 type NullString struct { 
  String string 
  Valid  bool // Valid is true if String is not NULL
}  

对于第二种办法,Scan 操作后需要先用 Valid 判断一下,为 true 则调用 String 或者 Value 方法,值得注意的并没有 sql.NullUint32 等类型,可以自定义 NULL 类型,实现 Scan 和 Value 方法。比如 NullInt32 的实现如下:

 // NullInt32 represents an int32 that may be null.
// NullInt32 implements the Scanner interface so
// it can be used as a scan destination, similar to NullString.
type NullInt32 struct {
    Int32 int32
    Valid bool // Valid is true if Int32 is not NULL
}

// Scan implements the Scanner interface.
func (n *NullInt32) Scan(value interface{}) error {
    if value == nil {
        n.Int32, n.Valid = 0, false
        return nil
    }
    n.Valid = true
    return convertAssign(&n.Int32, value)
}

// Value implements the driver Valuer interface.
func (n NullInt32) Value() (driver.Value, error) {
    if !n.Valid {
        return nil, nil
    }
    return int64(n.Int32), nil
}  

如你所见,上面的 Scan 的用法其实非常的不方便,实际项目里用 struct tag + orm 来做比较多,下面是使用 sqlx (go get 的例子:

 type User struct {
  ID        int       `db:"id"`
  Passport  string    `db:"passport"`
  Password  string    `db:"password"`
  Nickname  string    `db:"nickname"`
  CreatedAt time.Time `db:"create_time"`
}

func main() {
  var total int
  var user User
  var users []User
  var names []string
  db := session.GetSqlxDB()

  db.Get(&total, "SELECT COUNT(*) FROM user")
  fmt.Println(total)

  db.Get(&user, "SELECT * FROM user LIMIT 1")
  fmt.Println(user)

  db.Select(&users, "SELECT * FROM user")
  fmt.Println(users)

  db.Select(&names, "SELECT nickname FROM user")
  fmt.Println(names)

  db.QueryRowx("SELECT * FROM user LIMIT 1").StructScan(&user)
  fmt.Println(user)

  rows, _ := db.Queryx("SELECT * FROM user")
  defer rows.Close()
  for rows.Next() {
    var u User
    rows.StructScan(&u)
    fmt.Println(u)
  }
}  

除了好用的 Get、Select、StructScan,还有 MapScan、 SliceScan 分别对应 map[string]interface{} 和 []interface{},如果需要数据库 ORM 可以关注下 。

本章节的代码

相关文章