深入一点的 Go 数据库使用

Golang 中要使用数据库需要用到 database/sql 库,但是这个库只提供了基本的接口,不涉及具体的数据库操作实现,要操作数据库还需要引入一些额外的数据库驱动。在这里列举了部分数据库驱动,其中就包含今天重点要说明的 go-sql-driver

数据库连接池

在 Go 中使用 db, _ := sql.Open("mysql", "root:@/gotest") 来打开一个数据库对象。但是要注意调用 Open 之后并没有和数据库建立连接,只是简单的创建了一个对象方便后续使用。所以 Open 调用成功也不代表连接数据库成功。要判断是否连接成功可以调用 Ping()进行判断。

db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")
err := db.Ping()
if err != nil {
    fmt.print(err)
}

数据库连接池是由 database/sql 包维护的,使用的时候不用关心连接池的状态。这个库提供了几个函数来控制连接池的状态。

Prepared Statement

使用了预编译 (prepared statement) 可以避免 SQL 注入。预编译执行 SQL 过程如下:

  1. 客户端把 SQL 模板发送到 MySQL 服务端,SQL 语句中参数使用 ? 来占位,MySQL 会返回一个 id 来标示本次连接。
  2. 客户端把查询参数和 1 中返回的 id 发送给 MySQL 服务端进行查询,得到返回结果。
  3. SQL 执行完毕,调用 close 关闭数据库连接。

对于 Go 而言,代码过程和上面的流程有点不一样。由于使用的时候不维护数据库连接池状态,在 Go 中预编译的过程变成了下面的状态:

  1. 调用 prepare 函数告诉数据库连接池要进行 SQL 查询的预编译,数据库连接池会选择某一个连接,然后和 MySQL 服务端通信进行。stmt 会保存该连接,并不关心 MySQL 服务端返回的 id。
  2. 当执行 Exec 函数发送参数进行 SQL 查询,会尝试使用 1 中保存的连接去发送参数,如果该连接已经被关闭,或者在进行其他查询,那么会从连接池中重新选择一个新的连接再次进行 prepare,然后使用新的连接发送参数进行查询。

比较两个流程可以发现,Go 中屏蔽了代码和数据库的连接,代码只和 database/sql 维护的连接池交互。

但是这种设计会存在一个问题:在高并发的时候,一个连处理 prepare 之后又去处理新的逻辑,导致执行 Exec 在执行的时候使用新的连接重新 prepare

其他

package main

import (
	"database/sql"
	"fmt"
	"time"
)

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

type Article struct {
	ID      int
	Title   string
	Content string
	UserID  string
	Time    time.Time
}

func main() {
	db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")

	defer db.Close()

	err := db.Ping()
	if err != nil {
		fmt.Println(err)
	}

	article := new(Article)

	stmt, err := db.Prepare("select id,title,content,user_id,time from articles where id = ?") // ①
	defer stmt.Close()   // ③
	if err != nil {
		fmt.Println(err)
	}

	err = stmt.QueryRow(13).Scan(&article.ID, &article.Title, &article.Content, &article.UserID, &article.Time)  // ②
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(article)
}


以上面的代码为例,看下一些容易疑惑的问题。

Scan 函数参数的顺序是怎样子?

Scan 函数用来接收数据库返回的数据,如果 SQL 语句中 select 字段为 *,那么 Scan 函数的参数顺序应该和表结构定义的顺序一样。如果 SQL 语句中 select 指定了字段,那么 Scan 函数参数顺序应该和 SQL 语句中的指定的字段顺序一致。

数据库中字段为 varchar 类型,那么接收返回值的字段只能是 string 吗?

这个问题本质是会不会进行类型转换的问题,答案是会自动进行类型转换,Scan 在执行的时候会对类型进行判断,如果接收数据类型和表字段类型不一致,会隐式的帮你进行一次类型转换,如果转换失败就直接报错。

数据库中的 timestamp 如何转换成 Go 中的 time.Time

如果数据库中定义字段为 timestamp 类型,那么在 Go 中想要用 time.Time 类型来接收,在 open 数据库连接的时候需要加上 parseTime=true 的参数

db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")
*****
Written by JayChen on 08 November 2018