DBD::SQLite,如何通过占位符在查询中传递数组?
让我们有一个表:
sqlite> create table foo (foo int, bar int);
sqlite> insert into foo (foo, bar) values (1,1);
sqlite> insert into foo (foo, bar) values (1,2);
sqlite> insert into foo (foo, bar) values (1,3);
然后选择一些数据:
sqlite> select * from foo where foo = 1 and bar in (1,2,3);
1|1
1|2
1|3
工作正常。现在我尝试使用 DBD::SQLite 1.29:
my $sth = $dbh->prepare('select * from foo where foo = $1 and bar in ($2)');
$sth->execute(1,[1,2,3]);
这给了我空结果。 DBI 跟踪显示第二个占位符已正确绑定到数组,但没有分数。如果我加入
字符串中的数组值并传递它,则没有结果。如果我展平数组,我会得到“用 N 个占位符而不是 2 个占位符调用”的可预测错误。
我有点不知所措。还有什么可以尝试的?
更新:好吧,这是一个取自现实世界应用程序的真实示例。
首先,设置:我有几个充满统计数据的表,列数从 10 到 700+ 不等。我正在谈论的查询选择该数据的子集以用于报告目的。不同的报告考虑不同的方面,因此运行不同的查询,每个请求一个或多个。有200多个报告,即200-300个查询。这种方法是为 Postgres 开发的,现在我需要缩小它的规模并使其与 SQLite 一起使用。考虑到所有这些都适用于 Postgres,我不能证明检查所有查询并重写它们是合理的。不利于维护。我可以并且确实使用就地查询调整,例如用 IN () 替换 = ANY (),这些都是次要方面。
因此,这是我的示例:针对一份报告连续运行 2 个查询:
SELECT SPLIT, syn(SPLIT),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 40),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 30),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 50),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 220),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 20),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 80)
FROM csplit WHERE ACD = $1 AND SPLIT = $2
SELECT syn(LOGID), syn(LOC_ID), LOGID, EXTENSION, syn(ROLE), PERCENT,
syn(AUXREASON), syn(AWORKMODE), syn(DIRECTION), WORKSKILL, syn(WORKSKLEVEL),
AGTIME FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND LOC_ID = ANY ($3) AND
LOGID IS NOT NULL
这不是最复杂的示例,因为可以在查询的不同位置使用和重用任意数量的输入参数;用通用 ?
占位符替换它们并不是一项简单的任务。针对 Postgres 运行查询的代码看起来像这样(在输入清理等之后):
sub run_select {
my ($class, $dbh, $sql, @bind_values) = @_;
my $sth;
eval {
$sth = $dbh->prepare_cached($sql);
$sth->execute(@bind_values);
};
$@ and die "Error executing query: $@";
my %types;
{
my $dbt = $dbh->type_info_all;
@types{ map { $_->[1] } @$dbt[1..$#$dbt] } =
map { $_->[0] } @$dbt[1..$#$dbt];
};
my @result;
while (my $row = $sth->fetchrow_arrayref) {
my $i = 0;
push @result, [ map { [ $types{${$sth->{TYPE}}[$i++]}, $_ ] } @$row ];
};
return \@result;
};
我可以重写查询并直接注入值; SQL 注入并不是太大的威胁,因为所有输入在到达 SQL 引擎之前就已经通过正则表达式模式未被污染。我不想动态重写查询,原因有两个:a)它可能会导致值引用问题,b)它有点扼杀了prepare_cached背后的全部原因。如果每次都发生变化,SQL 引擎无法缓存重用准备好的语句。
正如我所说,上面的代码可以很好地与 Postgres 配合使用。由于SQLite引擎本身显然具有处理数据集的可能性,因此我认为这是DBD::SQLite实现的缺陷。所以真正的问题听起来像:有没有办法用 DBD::SQLite 在占位符中传递数据集?不一定是数组,尽管那是最合乎逻辑的。
Let's have a table:
sqlite> create table foo (foo int, bar int);
sqlite> insert into foo (foo, bar) values (1,1);
sqlite> insert into foo (foo, bar) values (1,2);
sqlite> insert into foo (foo, bar) values (1,3);
Then SELECT some data:
sqlite> select * from foo where foo = 1 and bar in (1,2,3);
1|1
1|2
1|3
Works all right. Now I'm trying to use DBD::SQLite 1.29:
my $sth = $dbh->prepare('select * from foo where foo = $1 and bar in ($2)');
$sth->execute(1,[1,2,3]);
And this gives me null results. DBI trace shows that 2nd placeholder is bound to array all right, but no score. If I join
array values in a string and pass it, no result. If I flatten the array, I get predictable error of "called with N placeholders instead of 2".
I'm kinda at loss. What else is there to try?
Upd: All right, here's one bona fide example taken from the real world application.
First, the setup: I have several tables filled with statistical data, number of columns varies from 10 to 700+. The queries I'm talking about select subset of that data for reporting purposes. Different reports consider different aspects and therefore run different queries, one or more per request. There are more than 200 reports, i.e. 200-300 queries. This approach was developed for Postgres and now I need to scale it down and make it work with SQLite. Considering that all this works well with Postgres, I can't justify going over all queries and rewriting them. Bad for maintenance. I can and do use in-place query adjustments, like replacing = ANY () with IN (), these are minor aspects.
So, here's my example: 2 queries ran in succession for one report:
SELECT SPLIT, syn(SPLIT),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 40),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 30),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 50),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 220),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 20),
(SELECT COUNT(*) FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND
LOC_ID = ANY ($3) AND LOGID IS NOT NULL AND WORKMODE = 80)
FROM csplit WHERE ACD = $1 AND SPLIT = $2
SELECT syn(LOGID), syn(LOC_ID), LOGID, EXTENSION, syn(ROLE), PERCENT,
syn(AUXREASON), syn(AWORKMODE), syn(DIRECTION), WORKSKILL, syn(WORKSKLEVEL),
AGTIME FROM cagent WHERE ACD = $1 AND SPLIT = $2 AND LOC_ID = ANY ($3) AND
LOGID IS NOT NULL
This is not the most complex example, as there can be any number of input parameters used and reused in different places in query; replacing them with generic ?
placeholders is not a trivial task. Code that runs queries against Postgres looks like that (after input cleansing et al):
sub run_select {
my ($class, $dbh, $sql, @bind_values) = @_;
my $sth;
eval {
$sth = $dbh->prepare_cached($sql);
$sth->execute(@bind_values);
};
$@ and die "Error executing query: $@";
my %types;
{
my $dbt = $dbh->type_info_all;
@types{ map { $_->[1] } @$dbt[1..$#$dbt] } =
map { $_->[0] } @$dbt[1..$#$dbt];
};
my @result;
while (my $row = $sth->fetchrow_arrayref) {
my $i = 0;
push @result, [ map { [ $types{${$sth->{TYPE}}[$i++]}, $_ ] } @$row ];
};
return \@result;
};
I can rewrite queries and inject values directly; SQL injection is not much of a threat because all input is untainted through regex patterns long before it can hit SQL engine. I don't want to rewrite queries dynamically for two reasons: a) it can potentially lead to problems with value quotation and b) it kinda kills the whole reason behind prepare_cached. SQL engine can't cache reuse prepared statement if it changes every time.
Now as I said, the code above works well with Postgres. Since SQLite engine itself obviously have the possibility of working with data sets, I thought it was a deficiency in DBD::SQLite implementation. So the real question sounds like: is there any way to pass a data set in a placeholder with DBD::SQLite? Not necessarily array though that would be most logical.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
试试这个:
您可以使用
x
重复运算符来生成所需数量的?
:Try this:
You can use the
x
repetition operator to generate the required number of?
s:使用 SQL::Abstract,如下所示:
Use SQL::Abstract, like this: