在Go中使用预编译语句有这些好处:安全,高效和便捷.但它们的实现方式可能和你的习惯有点不同,特别是它们需要和database/sql的一些内部功能协调工作.

预编译的语句和连接

在数据库层面,一条预编译语句会绑定一个单独的连接.典型的流程是,客户端发送一条带占位符的SQL语句给数据库进行预编译,服务器再返回该语句的ID,然后客户端通过发送ID和参数来执行语句.

然而在Go中,连接不是直接暴露给database/sql包的使用者.你不能在一个连接上预编译语句.你需要在DBTx中使用.而且database/sql有一些方便的功能比如自动重试.由于这些原因,在驱动层面需要关联预编译语句和连接,其隐藏在你的代码中.

它是这样工作的:

  1. 当你要预编译一个语句时,需从连接池中获取一个连接.
  2. Stmt对象记住使用了哪个连接.
  3. 当你执行Stmt时,它会尝试启用这个连接.如果它不可用,比如已是关闭或正在使用时,它会从连接池中重新获取另外一个连接,然后在新连接上重新预编译语句

因为语句在原始连接被占用时可以被重新预编译,在高并发的数据库中,这可能使得很多连接被使用或者创建了大量的预编译语句.这会导致明显的语句泄露,正在预编译的语句和要重新预编译的语句会比你想象的更多,甚至达到数据库对语句数量限制的上限.

避免预处理语句

Go会在幕后为你创建预编译语句.比如db.Query(sql, param1, param2),其作用是预编译一个sql,传入参数执行它,最后关闭语句.

有时候,预编译语句不是你想要的.有如下几种情况:

  1. 数据库不支持预编译语句.例如当使用MySQL驱动时,你可以连接到MemSQL和Sphinx上,因为它们支持MySQL的wire协议.但是它们不支持"binary"协议,其中就包含预编译语句,它们会因出现混乱而失败.
  2. 语句不能被重用使得使用它们有些不值,而且也需要用其他方式处理安全问题,所以这些性能开销是不需要的.在VividCortex的博客上就有这样的例子.

如果你不想使用预编译语句,那么你需要使用fmt.Sprint()或类试的方式来拼接SQL,将其作为唯一的参数传入db.Query()db.QueryRow().还有你的驱动需要支持纯文本的查询方式,其是通过Go1.1中添加的ExecerQueryer接口来支持的,文档在这里.

在事务中使用预编译语句

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)