最近看到不少朋友对 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 也支持事物,这部分信息大家可以参考其文档进行学习。
Leave a Reply