在Go中使用预编译语句有这些好处:安全,高效和便捷.但它们的实现方式可能和你的习惯有点不同,特别是它们需要和database/sql
的一些内部功能协调工作.
预编译的语句和连接
在数据库层面,一条预编译语句会绑定一个单独的连接.典型的流程是,客户端发送一条带占位符的SQL语句给数据库进行预编译,服务器再返回该语句的ID,然后客户端通过发送ID和参数来执行语句.
然而在Go中,连接不是直接暴露给database/sql
包的使用者.你不能在一个连接上预编译语句.你需要在DB
或Tx
中使用.而且database/sql
有一些方便的功能比如自动重试.由于这些原因,在驱动层面需要关联预编译语句和连接,其隐藏在你的代码中.
它是这样工作的:
- 当你要预编译一个语句时,需从连接池中获取一个连接.
Stmt
对象记住使用了哪个连接.- 当你执行
Stmt
时,它会尝试启用这个连接.如果它不可用,比如已是关闭或正在使用时,它会从连接池中重新获取另外一个连接,然后在新连接上重新预编译语句
因为语句在原始连接被占用时可以被重新预编译,在高并发的数据库中,这可能使得很多连接被使用或者创建了大量的预编译语句.这会导致明显的语句泄露,正在预编译的语句和要重新预编译的语句会比你想象的更多,甚至达到数据库对语句数量限制的上限.
避免预处理语句
Go会在幕后为你创建预编译语句.比如db.Query(sql, param1, param2)
,其作用是预编译一个sql,传入参数执行它,最后关闭语句.
有时候,预编译语句不是你想要的.有如下几种情况:
- 数据库不支持预编译语句.例如当使用MySQL驱动时,你可以连接到MemSQL和Sphinx上,因为它们支持MySQL的wire协议.但是它们不支持"binary"协议,其中就包含预编译语句,它们会因出现混乱而失败.
- 语句不能被重用使得使用它们有些不值,而且也需要用其他方式处理安全问题,所以这些性能开销是不需要的.在VividCortex的博客上就有这样的例子.
如果你不想使用预编译语句,那么你需要使用fmt.Sprint()
或类试的方式来拼接SQL,将其作为唯一的参数传入db.Query()
或db.QueryRow()
.还有你的驱动需要支持纯文本的查询方式,其是通过Go1.1中添加的Execer
和Queryer
接口来支持的,文档在这里.
在事务中使用预编译语句
Tx
会创建和绑定预编译语句,所以上文关于预编译的警告并不适用于这里.当你使用Tx
对象时,你的操作会被映射到底层的有且仅有的一个连接上.
这也意味着预编译语句是在Tx
内创建的,不可单独使用.同样,在DB
上创建的预编译语句也不能在事务中使用,因为它们会和不同的连接绑定.
若要在Tx
中使用事务外定义的预编译语句,那么你可以使用Tx.Stmt()
,它会通过事务外的预编译语句新建一个事务特有的语句.即它通过利用现有的预编译语句,设置连接到该事务,然后每次执行时预编译一次所有的语句.即使database/sql
源码中的TODO清单指明会改进它,但这些操作和实现还是不受欢迎的;我们建议不要这样用.
在事务中使用预编译语句时必须谨慎.考虑下例:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
_, err = stmt.Exec(i)
if err != nil {
log.Fatal(err)
}
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// stmt.Close() runs here!
在Go1.4之前,关闭*sql.Tx
时会将关联的连接释放回连接池,但关闭预编译语句会延迟到这(tx.Close)之后进行,这可能会导致并发地访问底层连接,使得连接的状态出现不一致.如果你使用Go1.4或之后的版本,你应该确保在事务提交或回滚前关闭statement.该issue已在Go1.4的CR131650043中被修复.
参数占位符的语法
预编译的参数占位符时数据库特有的,例如,比如MySQL,PostgreSQL和Oracle的:
MySQL | PostgreSQL | Oracle |
---|---|---|
WHERE col = ? | WHERE col = $1 | WHERE col = :col |
VALUES(?, ?, ?) | VALUES($1, $2, $3) | VALUES(:val1, :val2, :val3) |