预处理语句具有 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 有线协议。但是它们不支持包含预处理语句的「二进制」协议,因此它们可能以令人困惑的方式失败。
- 这些语句的重用程度不足以使它们变得有价值,而且安全问题以其他方式处理,因此性能开销是不需要的。这方面的一个例子可以在 VividCortex 博客 上看到。
如果您不想使用预处理语句,则需要使用 fmt.Sprint()
或类似的方法来组装 SQL,并将其作为唯一参数传递给db.Query()
或 db.QueryRow()
。而且您的驱动程序需要支持纯文本查询执行,这是通过 Execer
和 Queryer
接口在 Go 1.1 中添加的,文档在此。
事务中的预处理语句
在 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()在这里运行!
在 Go 1.4 之前,如果关闭一个 *sql.Tx
将与其关联的连接释放回池中,但延迟调用关闭预处理语句的执行,是发生在这种情况 之后 的,这可能导致对底层连接的并发访问,导致连接状态不一致。如果您使用 Go 1.4 或更早版本,则应确保在提交或回滚事务之前始终关闭该语句。这个问题 在 Go 1.4 中已由 CR 131650043 修复。
参数占位符语法
预处理语句中占位符参数的语法是特定于数据库的。例如,下面比较 MySQL、PostgreSQL 和 Oracle:
MySQL PostgreSQL Oracle
============ ================== =============
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)
转自:https://learnku.com/docs/go-database-sql/prepared/9479