英文:
How do I pass a []slice to an IN-condition in a prepared SQL statement with non-IN-conditions as well?
问题
假设你有以下的SQL查询语句:
SELECT *
FROM foo
WHERE type = ?
AND subtype IN (?)
同时你有以下可能的数据(我们假设一个用户界面可以设置这些数据):
var Type int
var SubTypes []int
在SubTypes
的情况下,我们谈论的是多选选择。
现在,以下的代码将无法工作:
rows, err := sqldb.Query(`SELECT *
FROM foo
WHERE type = ?
AND subtype IN (?)`, Type, SubTypes)
因为驱动程序(至少是在这个例子中使用的mysql
驱动程序)无法识别[]slice
。尝试将其拆开(SubTypes...
)也不起作用,因为A)你不能有多个拆开的参数,B)即使你可以,你的SQL只支持单个项目((?)
)。
英文:
Imagine you have the following SQL query:
SELECT *
FROM foo
WHERE type = ?
AND subtype IN (?)
And you have the following possible data (we imagine that a user interface can set these data):
var Type int
var SubTypes []int
In the case of SubTypes
, we are talking about a multiple choice selection.
Now, the following code won't work:
rows, err := sqldb.Query(`SELECT *
FROM foo
WHERE type = ?
AND subtype IN (?)`, Type, SubTypes)
Because the driver (at least the mysql
driver used in this example) doesn't recognise a []slice
. Typing to explode it (SubTypes...
) doesn't work either, because A) you cannot have more than one exploded parameter and B) even if you could, your SQL only supports a single item ((?)
).
答案1
得分: 4
然而,有一个解决方案。首先,由于我们只能有一个爆炸参数而没有其他参数,我们应该将参数放在一个单独的[]slice
中:
var params []interface{}
params = append(params, Type)
for _, subtype := range SubTypes {
params = append(params, SubTypes)
}
由于SQL不会自动展开,让我们展开这个循环:
var params []interface{}
params = append(params, Type)
inCondition := ""
for _, subtype := range SubTypes {
params = append(params, SubTypes)
if inCondition != "" {
inCondition += ", "
}
inCondition += "?"
}
假设SubTypes
包含[]int{1,2,3}
,inCondition
现在应该包含?, ?, ?
。
然后,我们将其与SQL语句组合起来,并展开参数:
sqlstr := fmt.Sprintf(`SELECT *
FROM foo
WHERE type = ?
AND subtype IN (%s)`, inCodition)
rows, err := sqldb.Query(sqlstr, params...)
当然,如果您可以直接将[]slice
传递给预处理语句并自动展开,那将非常方便。但是,如果您处理更多的“未知”数据,可能会导致一些意外的结果。
英文:
However, there is a solution. First of all, since we can only have a single exploding parameter and no others, we should first put together our parameters in a single []slice
:
var params []interface{}
params = append(params, Type)
for _, subtype := range SubTypes {
params = append(params, SubTypes)
}
Since the SQL will not expand on its own, let's expand that loop:
var params []interface{}
params = append(params, Type)
inCondition := ""
for _, subtype := range SubTypes {
params = append(params, SubTypes)
if inCondition != "" {
inCondition += ", "
}
inCondition += "?"
}
Assuming SubTypes
contains []int{1,2,3}
, inCondition
should now contain ?, ?, ?
.
We then combine that to our SQL statement and explode the argument:
sqlstr := fmt.Sprintf(`SELECT *
FROM foo
WHERE type = ?
AND subtype IN (%s)`, inCodition)
rows, err := sqldb.Query(sqlstr, params...)
Of course, it would be pretty cool, if you could simply pass []slice
s to your prepared statements, and the automatically expanded. But that might give some unexpected results if you are dealing with more 'unknown' data.
答案2
得分: 4
预编译语句并不是这样工作的,至少在我所了解的主要数据库管理系统中不是这样。我的意思是,在Go语言中,由database/sql
驱动程序实现的预编译语句支持应该使用底层数据库管理系统提供的相应功能(如果驱动程序与其交互的数据库引擎没有提供此类支持,驱动程序可以选择模拟此类支持)。
在我熟悉的所有数据库管理系统中,预编译语句的整个思想是它由数据库引擎处理一次并进行缓存;这里的“处理”意味着语法检查、编译成某种特定于数据库的内部表示形式,并确定其执行计划。从“编译”一词可以看出,语句的文本只被处理一次,然后对预编译语句的每次调用实际上只是告诉服务器“这是我之前提供给你的那个预编译语句的ID,这是要用于其中占位符的实际参数列表”。这就像编译一个Go程序,然后用不同的命令行标志连续多次调用它一样。
所以你提出的解决方案是正确的:如果你想在调用之间操纵语句文本,那么可以使用客户端的文本操作<sup>1</sup>,但是除非你真的打算多次执行生成的文本,否则不要尝试将其作为预编译语句使用。
为了更清楚一些:你最初尝试准备类似于
SELECT a, b FROM foo WHERE a IN (?)
的语句,可能在尝试为IN (?)
占位符提供一组值时失败,因为逗号在那里是用来指定多个值的*语法,而不是值的一部分。
我认为仍然可以准备类似于
SELECT a, b FROM foo WHERE a IN (?, ?, ?)
的语句,因为它不违反这个规则。不过这对你来说可能不是一个解决方案...
另请参阅这个链接和这个链接——研究后者将使你能够直接在MySQL客户端中使用预编译语句。
<sup>1</sup> 一些引擎提供了生成服务器端SQL并执行生成的文本的功能。
英文:
Prepared statements do not work that way, at least not in major DBMS I know. I mean, in Go, the support for prepared statements implemented by database/sql
drivers is supposed to use the corresponding facility provided by the underlying DBMS (a driver might opt to simulate such support if it's not provided by the DB engine it interfaces with).
Now in all the DBMS-s I'm familiar with, the whole idea of prepared statement is that it's processed once by the DB engine and cached; "processed" here means syntax checking, compiling into some DB-specific internal representation and its execution plan figured out. As follows from the term "compiled", the statement's text is processed exactly once, and then each call to the prepared statement just essentially tells the server "here is the ID of that prepared statement I supplied you earlier, and here's the list of actual parameters to use for placeholders it contained". It's like compiling a Go program and then calling it several times in a row with different command-line flags.
So the solution you have come up with is correct: if you want to mess with the statement text between invocation then by all means use client-side text manipulations<sup>1</sup> but do not attempt to use the result of it as a prepared statement unless you really intend to execute the resulting text more than once.
And to be may be more clear: your initial attempt to prepare something like
SELECT a, b FROM foo WHERE a IN (?)
supposedly fails at your attempt to supply a set of values for that IN (?)
placeholder because commas which would be required there to specify several values are syntax, not parts of the value.
I think it should still be fine to prepare something like
SELECT a, b FROM foo WHERE a IN (?, ?, ?)
because it does not break that rule. Not that it's a solution for you…
See also this and this — studying the latter would allow you to play with prepared statements directly in the MySQL client.
<sup>1</sup> Some engines provide for server-side SQL generation with subsequent execution of the generated text.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论