具有可变大小变量列表的 MySQL 准备语句
Posted
技术标签:
【中文标题】具有可变大小变量列表的 MySQL 准备语句【英文标题】:MySQL Prepared statements with a variable size variable list 【发布时间】:2010-09-24 13:05:05 【问题描述】:您将如何在 php 中编写一个准备好的 mysql 语句,它每次使用不同数量的参数?此类查询的一个示例是:
SELECT `age`, `name` FROM `people` WHERE id IN (12, 45, 65, 33)
IN
子句每次运行时都会有不同数量的id
s。
我有两种可能的解决方案,但想看看是否有更好的方法。
可能的解决方案 1 使语句接受 100 个变量,其余的用保证不在表中的虚拟值填充;多次调用超过 100 个值。
可能的解决方案 2 不要使用准备好的语句;为可能的注入攻击构建和运行查询检查。
【问题讨论】:
我怀疑我会避免使用您建议的两种解决方案。准备好的语句使保护您的代码更加容易。如果您的列表很小,那么您的第一个解决方案似乎效率低下且浪费。 我同意我认为这些想法可能会激发其他人的思想。 :) 对于 SQL Server,请参阅Parameterizing an SQL IN clause? 【参考方案1】:我能想到几个解决方案。
一种解决方案可能是创建一个临时表。为 in 子句中的每个参数插入表中。然后对你的临时表做一个简单的连接。
另一种方法可能是这样做。
$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$parmcount=count($parms); // = 4
$inclause=implode(',',array_fill(0,$parmcount,'?')); // = ?,?,?,?
$sql='SELECT age, name FROM people WHERE id IN (%s)';
$preparesql=sprintf($sql,$inclause); // = example statement used in the question
$st=$dbh->prepare($preparesql);
$st->execute($parms);
我怀疑,但没有证据表明,第一个解决方案可能更适合较大的列表,而后者适用于较小的列表。
为了让@orrd 开心,这里有一个简洁的版本。
$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$st=$dbh->prepare(sprintf('SELECT age, name FROM people WHERE id IN (%s)',
implode(',',array_fill(0,count($parms),'?'))));
$st->execute($parms);
【讨论】:
我喜欢你的第二个建议。做它并忘记它,直到性能成为问题。到那时,可能值得研究第一个选项。 要是我能想到就好了!您的第一个解决方案听起来正是我正在寻找的东西。 我经常使用模式#2。 Perl 的 DBI 有一个 prepare_cached() 函数,因此如果您使用相似数量的占位符进行查询,它将重用语句句柄。虽然不确定 PHP.. 除非数据集很大,否则创建一个临时表是多余的。示例代码过于复杂(可以消除一半的变量,并且可以通过做更多的内联工作来大大简化代码)。但基本思路是好的,使用 implode(',',array_fill(0,count($params),'?')) 生成“?”,然后将 $params 作为数据传递给绑定. @orrd,嗯...当我撰写此答案时,我试图使其冗长,以便易于理解。我同意,它可以被简化,但我不认为你建议的简化会使答案更容易理解,或者对性能有任何重大影响。无论如何,我会更新我的答案并为您添加一个精简版。【参考方案2】:我今天有一个类似的问题,我发现了这个话题。查看答案并在谷歌周围搜索,我找到了一个很好的解决方案。
虽然,我的问题有点复杂。 因为我也有固定的绑定值和动态的。
这就是解决方案。
$params = array()
$all_ids = $this->get_all_ids();
for($i = 0; $i <= sizeof($all_ids) - 1; $i++)
array_push($params, $all_ids[$i]['id']);
$clause = implode(',', array_fill(0, count($params), '?')); // output ?, ?, ?
$total_i = implode('', array_fill(0, count($params), 'i')); // output iiii
$types = "ss" . $total_i; // will reproduce : ssiiii ..etc
// %% it's necessary because of sprintf function
$query = $db->prepare(sprintf("SELECT *
FROM clients
WHERE name LIKE CONCAT('%%', ?, '%%')
AND IFNULL(description, '') LIKE CONCAT('%%', ?, '%%')
AND id IN (%s)", $clause));
$thearray = array($name, $description);
$merge = array_merge($thearray, $params); // output: "John", "Cool guy!", 1, 2, 3, 4
// We need to pass variables instead of values by reference
// So we need a function to that
call_user_func_array('mysqli_stmt_bind_param', array_merge (array($query, $types), $this->makeValuesReferenced($merge)));
而函数makeValuesreferenced:
public function makeValuesReferenced($arr)
$refs = array();
foreach($arr as $key => $value)
$refs[$key] = &$arr[$key];
return $refs;
获取此“诀窍”的链接:https://bugs.php.net/bug.php?id=49946、PHP append one array to another (not array_push or +)、[PHP]: Error -> Too few arguments in sprintf();、http://no2.php.net/manual/en/mysqli-stmt.bind-param.php#89171、Pass by reference problem with PHP 5.3.1
【讨论】:
【参考方案3】:还有FIND_IN_SET
function,它的第二个参数是一串逗号分隔值:
SELECT age, name FROM people WHERE FIND_IN_SET(id, '12,45,65,33')
【讨论】:
这里的问题是:它不会使用主索引,默认为全表扫描,同时对每一行执行 FIND_IN_SET。【参考方案4】:我的回答来自:http://bugs.php.net/bug.php?id=43568 这是我对我的问题的有效解决方案。现在我可以根据需要动态使用任意数量的参数。它们将与我在数组中的数字相同,或者在这种情况下,我将最后一个查询(找到 email = 'johndoe@gmail.com' 的所有 id)中的 id 传递给动态查询以获取所有无论我最终需要多少,关于这些 id 的信息。
<?php $NumofIds = 2; //this is the number of ids i got from the last query
$parameters=implode(',',array_fill(0,$NumofIds,'?'));
// = ?,? the same number of ?'s as ids we are looking for<br />
$paramtype=implode('',array_fill(0,$NumofIds,'i')); // = ii<br/>
//make the array to build the bind_param function<br/>
$idAr[] = $paramtype; //'ii' or how ever many ?'s we have<br/>
while($statement->fetch()) //this is my last query i am getting the id out of<br/>
$idAr[] = $id;
//now this array looks like this array:<br/>
//$idAr = array('ii', 128, 237);
$query = "SELECT id,studentid,book_title,date FROM contracts WHERE studentid IN ($parameters)";
$statement = $db->prepare($query);
//build the bind_param function
call_user_func_array (array($statement, "bind_param"), $idAr);
//here is what we used to do before making it dynamic
//statement->bind_param($paramtype,$v1,$v2);
$statement->execute();
?>
【讨论】:
【参考方案5】:如果您只在 IN
子句中使用整数值,那么没有什么可以反对在不使用 SQL 参数的情况下动态构建查询。
function convertToInt(&$value, $key)
$value = intval($value);
$ids = array('12', '45', '65', '33');
array_walk($ids, 'convertToInt');
$sql = 'SELECT age, name FROM people WHERE id IN (' . implode(', ', $ids) . ')';
// $sql will contain SELECT age, name FROM people WHERE id IN (12, 45, 65, 33)
但毫无疑问,here 的解决方案是解决此问题的更通用方法。
【讨论】:
为什么要解决“X 如何使用准备好的语句?”动态构建不同的查询?如果您使用准备好的语句来重用缓存的查询计划,那么您已经破坏了这一点。如果你这样做是为了防止 SQL 注入,那是不同的。【参考方案6】:不错的 sql 包装器支持绑定到数组值。 即
$sql = "... WHERE id IN (?)";
$values = array(1, 2, 3, 4);
$result = $dbw -> prepare ($sql, $values) -> execute ();
【讨论】:
我实际上不知道任何允许绑定数组类型参数的 MySQL(mysql、mysqli 和 PDO)本机 PHP 数据库访问库。 几年前我在 php 开发时,adodb 为我做了非常好的工作。我认为你应该检查一下。 执行此操作的任何框架都是通过扩展列表并将其插入到 prepare() 之前的 SQL 查询中来实现的。和绑定参数不一样。【参考方案7】:请把#2 从桌子上拿下来。准备好的语句是您应该考虑保护自己免受 SQL 注入的唯一方法。
但是,您可以做的是生成一组动态绑定变量。也就是说,如果你需要 7 个(或 103 个),就不要赚 100 个。
【讨论】:
什么?那没有意义。他正在使用准备好的语句,但他正在动态设置占位符的数量。 在场景 #1 中,他静态定义查询以获取 100 个参数,在 #2 中,他没有使用准备好的语句。我的建议是使用绑定动态构建查询,这与您所说的相同。 哎呀。我正在阅读来自***.com/questions/327274/… 的#2。对不起!以上是关于具有可变大小变量列表的 MySQL 准备语句的主要内容,如果未能解决你的问题,请参考以下文章
如何在 MySQL 的“EXECUTE STATEMENT USING”语句中使用变量列表?