【问题标题】:DBD::SQLite, how to pass array in query via placeholder?DBD::SQLite,如何通过占位符在查询中传递数组?
【发布时间】:2011-04-10 15:47:08
【问题描述】:

我们来张桌子:

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 跟踪显示第二个占位符已绑定到数组,但没有得分。如果我在一个字符串中join 数组值并传递它,则没有结果。如果我展平数组,我会得到“使用 N 个占位符而不是 2 调用”的可预测错误。

我有点不知所措。还有什么可以尝试的?

更新:好的,这是一个来自真实世界应用程序的真实示例。

首先,设置:我有几张表填充了统计数据,列数从 10 到 700+ 不等。我正在谈论的查询选择该数据的子集以用于报告目的。不同的报告考虑不同的方面,因此运行不同的查询,每个请求一个或多个。有200多个报告,即200-300个查询。这种方法是为 Postgres 开发的,现在我需要将其缩小并使其与 SQLite 一起使用。考虑到所有这些都适用于 Postgres,我不能证明遍历所有查询并重写它们是合理的。不利于维护。我可以并且确实使用就地查询调整,例如将 = ANY () 替换为 IN (),这些都是次要方面。

因此,这是我的示例:针对一份报告连续运行 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 在占位符中传递数据集?不一定是数组,但这是最合乎逻辑的。

【问题讨论】:

  • 或许您应该发布一个实际示例,说明您从应用程序/库中获取的数据,而不是发布虚构的代码。
  • -1 这个问题质量低

标签: arrays perl sqlite placeholder


【解决方案1】:

试试这个:

my $sth = $dbh->prepare("select * from foo where foo = ? and bar in (?,?,?)";
$sth->execute(1,1,2,3);

您可以使用x重复运算符生成所需数量的?s:

my $sql = sprintf "select ... and bar in (%s)", join ",", ('?')x@values; 

【讨论】:

  • 不能。首先,查询是从调用应用程序(我的是一个库)连同参数一起传递给我的,我不想全部解析。它很容易比上面的简化示例复杂得多。其次,这种方法首先破坏了整个占位符的想法——如果我必须进行解析,我最好将值直接注入查询文本以避免头痛。
  • 它真的完全没有破坏这个想法,因为注入值需要正确转义值,传递多个值会加剧这个问题。逗号分隔值。插入多个 ?s 时,您没有进行任何解析。
  • @Alexander:当你搞砸 SQL 引用时发生的头痛是非常真实的。 不要自己动手!(如果这样做,您最终可能会遇到完全困扰 PHP 应用程序的那种 SQL 注入漏洞。)
  • @MkV:我一直认为转义是使用占位符的次要优势。恕我直言,它们的主要价值在于语句抽象、准备语句的缓存和显着的查询加速。可能不是 SQLite,尽管我没有运行任何基准来证明或反驳这一点。
  • @Donal:别担心,偏执狂来拯救。没有可信输入之类的东西,所有这些都通过环保、可生物降解但可重复使用的高腐蚀性正则表达式彻底清洗。
【解决方案2】:

使用SQL::Abstract,像这样:

use strict;
use warnings;
use SQL::Abstract;

my $sqla = SQL::Abstract->new;
my %where  = (
    foo => 1,
    bar => { -in => [1,2,3] }
);

my ($sql, @params) = 
    $sqla->select('foo', '*', \%where);

my $sth = $dbh->prepare($sql);
$sth->execute(@params);

【讨论】:

  • 它看起来不够抽象,无法满足我的需求。见上面的例子。不过感谢您的建议,我会记住该模块以备将来使用。
猜你喜欢
  • 1970-01-01
  • 2013-08-23
  • 2018-07-08
  • 2012-10-23
  • 1970-01-01
  • 1970-01-01
  • 2016-02-23
  • 2021-02-04
相关资源
最近更新 更多