MyMySQL 的 database/sql 接口使用

最近看到不少朋友对 Golang 中操作 MySQL 数据库有疑问,那么就此内容给大家分享一下吧。

MyMySQL 的原作者是来自波兰的 ziutek,他根据 MySQL 的协议标准使用 Golang 实现了 MyMySQL 包。根据他的介绍,这个包可以用在 MySQL 4.1 或更高版本上,并且在 5.0、5.1 版本上经过项目的实际验证。

只要用 MySQL 做过项目的朋友一定遇到过编码问题,set names 几乎成为了中文环境下使用 MySQL 的标配。那么 Golang 也不能例外。不过由于 database/sql 使用了随机的连接池,且未提供任何方法让所有连接都执行某个操作,于是这个麻烦事儿就落到了驱动的肩上。对于中文什么的,大老外一向不怎么在意。我提交了这个 issue 给 ziutek,不过对于他最终实现的方案不怎么满意。需要额外编写godrv.Register 来对编码进行设置。

同时当连接空闲时间超出服务器的 wait_timeout 时,会出现 broken pipe。对于这个问题,ziutek 的建议是使用 MyMySQL 的 autorc,但是 godrv 并没有基于 autorc 实现 database/sql。换句话说,要解决这个错误,只能使用 MyMySQL 原始接口。

鉴于这些问题,我只能 fork 了 ziutek 的项目,对 MyMySQL 打上相应的补丁。主要是针对编码设置进行了改进,并顺便实现了 keepalive 功能。

本文主要介绍 fork 的版本的使用。

在 fork 的版本中,还有一个改进就是统一了 go get 的安装接口,无需再逐一安装 MyMySQl 的子包,只需要

go get github.com/mikespook/mymysql

即可完成安装。

关于使用,还是用代码说话吧,完整代码看这里。为了通用,这里只介绍使用 database/sql 接口的使用。在实际项目中,我也建议大家尽量使用该接口,这样在最大可用性保障下保持兼容性。

import(
    "log"
    "database/sql"
    _ "github.com/mikespook/mymysql/godrv"
)

使用 MyMySQL 的 database/sql 接口要导入 godrv 包。在这个包的 init 函数中自动注册了 database/sql 的驱动,所以导入后无需再使用。

db, err := sql.Open("mymysql", "tcp://127.0.0.1:3306/test/root/xxiyy?charset=utf8&keepalive=1200")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

使用 mymysql 驱动打开一个 sql.DB 连接。dsn 连接串是我对 ziutek 的 MyMySQL 的主要改进。支持多种连接方式:

  • 使用 tcp 协议:[tcp://addr/]dbname/user/password[?params]
  • 使用 unix sock:[unix://sockpath/]dbname/user/password[?params]

其中 tcp 协议的 addr 必须是含有主机名或 ip,且包含又分号分隔的端口号的字符串。如 localhost:3306、192.168.3.2:3307。unix sock 协议中的 sockpath 必须是 MySQL 的 sock 文件的绝对路径。

在上述 dsn 中,方括号内的是可选的内容,可省略。当协议信息(第一个方括号内)未指定时,使用 tcp://127.0.0.1:3306/ 作为默认值。

params 部分是用于当前数据库驱动的参数设置,当前来说,只有两个可设置的参数:

  • charset:用于 ‘set names’ 设置连接编码。
  • keepalive:每 keepalive 秒向服务器发送 PING。

需要特别强调的是,如果密码含有斜线(/),由于解析规则的缘故,需要用星号(*)代替。如果密码含有星号(*),则需要用两个星号(**)代替。例如:

  • 原密码 [pass/wd],在 dsn 中应写为 [pass*wd]。
  • 原密码 [pass*wd],在 dsn 中应写为 [pass**wd]。
stmt, err := db.Prepare("insert into `test` (`key`, `value`) values (?, ?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()
rslt, err := stmt.Exec("name", "foobar")
if err != nil {
    log.Fatal(err)
}
if a, err := rslt.RowsAffected(); err != nil {
    log.Print(err)
} else {
    log.Printf("[INS]Affected rows=%d", a)
}
if id, err := rslt.LastInsertId(); err != nil {
    log.Print(err)
} else {
    log.Printf("[INS]Last insert id=%d", id)
}

在 database/sql 接口里,有多种办法执行某个 SQL 语句。出于安全考虑,强烈建议大家不要使用字符串方式直接拼接 SQL 语句进行执行,如果可能,在大多数其他接口中都会出现的 Prepare 方法是首选。执行完 SQL 语句后,可以通过 sql.Result 得到这次执行所影响的行数,和最后插入的 Id 值。如果表中没有使用 autoincrement 作为主键,那么这个 Id 值永远为 0。

rows, err := db.Query("select * from `test`")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    var k, v string
    rows.Scan(&k, &v)
    log.Printf("[ROWS]key=%s, value=%s", k, v)
}

查询一个二维表,从中取得多行数据使用 Query 方法。需要注意的是 sql.Rows 的 Scan 方法接收的参数必须是指针。也就是说存放数据的地址要在 Scan 调用前准备好。Scan 内部使用了反射来识别参数类型并进行赋值,也可以传递一个 []interface{}。这时必须这样调用:rows.Scan(a…) 才能得到预期结果。

row := db.QueryRow("select * from `test` where `key` = ?", "name")
var k, v string
row.Scan(&k, &v)
log.Printf("[ROW]key=%s, value=%s", k, v)

对于有些如用主键进行的查询,仅仅会读出一行数据。那么可以用 QueryRow 进行更加简化的读取。

rslt, err = db.Exec("delete from `test`")
if a, err := rslt.RowsAffected(); err != nil {
    log.Print(err)
} else {
    log.Printf("[DEL]Affected rows=%d", a)
}
if id, err := rslt.LastInsertId(); err != nil {
    log.Print(err)
} else {
    log.Printf("[DEL]Last insert id=%d", id)
}

对于没有拼接 SQL 需要的操作,可以直接在 sql.DB 对象上调用 Exec 方法。返回的 sql.Result 与 sql.Stmt 的 Exec 方法一致。

关于基础的使用,大致就这些内容。database/sql 也支持事物,这部分信息大家可以参考其文档进行学习。

Join the Conversation

5 Comments

  1. 是我理解错了,select * from `test`时里面所有字段都必须在row.Scan()写出来,要不全部内容都不会显示出来。

  2. 你的改动有push给原作者吗?最好能合并一下,要不你就要一直维护下去了。否则不敢用啊。期待回复

Leave a comment

Your email address will not be published. Required fields are marked *