Могу ли я связать массив с условием IN()?
Мне любопытно узнать, возможно ли связать массив значений с заполнителем с помощью PDO. Вариант использования здесь пытается передать массив значений для использования с IN()
состояние.
Я хотел бы иметь возможность сделать что-то вроде этого:
<?php
$ids=array(1,2,3,7,8,9);
$db = new PDO(...);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN(:an_array)'
);
$stmt->bindParam('an_array',$ids);
$stmt->execute();
?>
И пусть PDO связывает и цитирует все значения в массиве.
На данный момент я делаю:
<?php
$ids = array(1,2,3,7,8,9);
$db = new PDO(...);
foreach($ids as &$val)
$val=$db->quote($val); //iterate through array and quote
$in = implode(',',$ids); //create comma separated list
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN('.$in.')'
);
$stmt->execute();
?>
Что, безусловно, делает работу, но просто интересно, есть ли встроенное решение, которое мне не хватает?
23 ответа
Я думаю, что Соулмердж прав. вам придется построить строку запроса.
<?php
$ids = array(1, 2, 3, 7, 8, 9);
$inQuery = implode(',', array_fill(0, count($ids), '?'));
$db = new PDO(...);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN(' . $inQuery . ')'
);
// bindvalue is 1-indexed, so $k+1
foreach ($ids as $k => $id)
$stmt->bindValue(($k+1), $id);
$stmt->execute();
?>
исправить: Дэн, ты был прав. исправил код (хотя не проверял)
редактировать: и Крис (комментарии), и кто-то не может поспорить, что цикл foreach...
(...)
// bindvalue is 1-indexed, so $k+1
foreach ($ids as $k => $id)
$stmt->bindValue(($k+1), $id);
$stmt->execute();
... может быть избыточным, поэтому foreach
петля и $stmt->execute
можно заменить просто...
<?php
(...)
$stmt->execute($ids);
?>
(опять же, я не проверял это)
Для чего-то быстрого:
//$db = new PDO(...);
//$ids = array(...);
$qMarks = str_repeat('?,', count($ids) - 1) . '?';
$sth = $db->prepare("SELECT * FROM myTable WHERE id IN ($qMarks)");
$sth->execute($ids);
Это так важно использовать IN
заявление? Попробуй использовать FIND_IN_SET
соч.
Например, в PDO есть запрос
SELECT * FROM table WHERE FIND_IN_SET(id, :array)
Тогда вам нужно всего лишь связать массив значений, набитых запятой, как этот
$ids_string = implode(',', $array_of_smth); // WITHOUT WHITESPACES BEFORE AND AFTER THE COMMA
$stmt->bindParam('array', $ids_string);
и это сделано.
UPD: Как некоторые люди указали в комментариях к этому ответу, есть некоторые вопросы, которые должны быть четко изложены.
FIND_IN_SET
не использует индекс в таблице, и он все еще не реализован - посмотрите эту запись в трекере ошибок MYSQL. Спасибо @BillKarwin за уведомление.- Вы не можете использовать строку с запятой внутри в качестве значения массива для поиска. Невозможно разобрать такую строку после того, как
implode
так как вы используете символ запятой в качестве разделителя. Спасибо @VaL за примечание.
В общем, если вы не сильно зависите от индексов и не используете строки с запятой для поиска, мое решение будет намного проще, проще и быстрее, чем решения, перечисленные выше.
Поскольку я делаю много динамических запросов, это очень простая вспомогательная функция, которую я сделал.
public static function bindParamArray($prefix, $values, &$bindArray)
{
$str = "";
foreach($values as $index => $value){
$str .= ":".$prefix.$index.",";
$bindArray[$prefix.$index] = $value;
}
return rtrim($str,",");
}
Используйте это так:
$bindString = helper::bindParamArray("id", $_GET['ids'], $bindArray);
$userConditions .= " AND users.id IN($bindString)";
Возвращает строку :id1,:id2,:id3
а также обновляет ваш $bindArray
привязок, которые вам понадобятся, когда придет время выполнить ваш запрос. Легко!
Очень простой способ для postgres- использовать массив postgres ("{}"):
$ids = array(1,4,7,9,45);
$param = "{".implode(', ',$ids)."}";
$cmd = $db->prepare("SELECT * FROM table WHERE id = ANY (?)");
$result = $cmd->execute(array($param));
Решение от EvilRygy не сработало для меня. В Postgres вы можете сделать еще один обходной путь:
$ids = array(1,2,3,7,8,9);
$db = new PDO(...);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id = ANY (string_to_array(:an_array, ','))'
);
$stmt->bindParam(':an_array', implode(',', $ids));
$stmt->execute();
Вот мое решение:
$total_items = count($array_of_items);
$question_marks = array_fill(0, $total_items, '?');
$sql = 'SELECT * FROM foo WHERE bar IN (' . implode(',', $question_marks ). ')';
$stmt = $dbh->prepare($sql);
$stmt->execute(array_values($array_of_items));
Обратите внимание на использование array_values. Это может исправить ключевые проблемы заказа.
Я сливал массивы идентификаторов, а затем удалял дублирующиеся элементы. У меня было что-то вроде:
$ids = array(0 => 23, 1 => 47, 3 => 17);
И это не удалось.
Когда у вас есть другой параметр, вы можете сделать так:
$ids = array(1,2,3,7,8,9);
$db = new PDO(...);
$query = 'SELECT *
FROM table
WHERE X = :x
AND id IN(';
$comma = '';
for($i=0; $i<count($ids); $i++){
$query .= $comma.':p'.$i; // :p0, :p1, ...
$comma = ',';
}
$query .= ')';
$stmt = $db->prepare($query);
$stmt->bindValue(':x', 123); // some value
for($i=0; $i<count($ids); $i++){
$stmt->bindValue(':p'.$i, $ids[$i]);
}
$stmt->execute();
Я расширил PDO, чтобы сделать что-то похожее на то, что предлагает stefs, и мне было легче в долгосрочной перспективе:
class Array_Capable_PDO extends PDO {
/**
* Both prepare a statement and bind array values to it
* @param string $statement mysql query with colon-prefixed tokens
* @param array $arrays associatve array with string tokens as keys and integer-indexed data arrays as values
* @param array $driver_options see php documention
* @return PDOStatement with given array values already bound
*/
public function prepare_with_arrays($statement, array $arrays, $driver_options = array()) {
$replace_strings = array();
$x = 0;
foreach($arrays as $token => $data) {
// just for testing...
//// tokens should be legit
//assert('is_string($token)');
//assert('$token !== ""');
//// a given token shouldn't appear more than once in the query
//assert('substr_count($statement, $token) === 1');
//// there should be an array of values for each token
//assert('is_array($data)');
//// empty data arrays aren't okay, they're a SQL syntax error
//assert('count($data) > 0');
// replace array tokens with a list of value tokens
$replace_string_pieces = array();
foreach($data as $y => $value) {
//// the data arrays have to be integer-indexed
//assert('is_int($y)');
$replace_string_pieces[] = ":{$x}_{$y}";
}
$replace_strings[] = '('.implode(', ', $replace_string_pieces).')';
$x++;
}
$statement = str_replace(array_keys($arrays), $replace_strings, $statement);
$prepared_statement = $this->prepare($statement, $driver_options);
// bind values to the value tokens
$x = 0;
foreach($arrays as $token => $data) {
foreach($data as $y => $value) {
$prepared_statement->bindValue(":{$x}_{$y}", $value);
}
$x++;
}
return $prepared_statement;
}
}
Вы можете использовать это так:
$db_link = new Array_Capable_PDO($dsn, $username, $password);
$query = '
SELECT *
FROM test
WHERE field1 IN :array1
OR field2 IN :array2
OR field3 = :value
';
$pdo_query = $db_link->prepare_with_arrays(
$query,
array(
':array1' => array(1,2,3),
':array2' => array(7,8,9)
)
);
$pdo_query->bindValue(':value', '10');
$pdo_query->execute();
Для меня более сексуальное решение - построить динамический ассоциативный массив и использовать его.
// a dirty array sent by user
$dirtyArray = ['Cecile', 'Gilles', 'Andre', 'Claude'];
// we construct an associative array like this
// [ ':name_0' => 'Cecile', ... , ':name_3' => 'Claude' ]
$params = array_combine(
array_map(
// construct param name according to array index
function ($v) {return ":name_{$v}";},
// get values of users
array_keys($dirtyArray)
),
$dirtyArray
);
// construct the query like `.. WHERE name IN ( :name_1, .. , :name_3 )`
$query = "SELECT * FROM user WHERE name IN( " . implode(",", array_keys($params)) . " )";
// here we go
$stmt = $db->prepare($query);
$stmt->execute($params);
Глядя на PDO: предопределенные константы, нет PDO::PARAM_ARRAY, который вам понадобится, как указано в PDOStatement->bindParam.
bool PDOStatement::bindParam (смешанный $ параметр, смешанный &$ переменная [, int $ data_type [, int $ длина [, смешанный $driver_options ]]])
Поэтому я не думаю, что это достижимо.
Я также понимаю, что этот поток старый, но у меня была уникальная проблема, когда при преобразовании устаревшего драйвера mysql в драйвер PDO мне нужно было создать функцию, которая могла бы динамически создавать как обычные параметры, так и IN из одного и того же массив параметров. Поэтому я быстро построил это:
/**
* mysql::pdo_query('SELECT * FROM TBL_WHOOP WHERE type_of_whoop IN :param AND siz_of_whoop = :size', array(':param' => array(1,2,3), ':size' => 3))
*
* @param $query
* @param $params
*/
function pdo_query($query, $params = array()){
if(!$query)
trigger_error('Could not query nothing');
// Lets get our IN fields first
$in_fields = array();
foreach($params as $field => $value){
if(is_array($value)){
for($i=0,$size=sizeof($value);$i<$size;$i++)
$in_array[] = $field.$i;
$query = str_replace($field, "(".implode(',', $in_array).")", $query); // Lets replace the position in the query string with the full version
$in_fields[$field] = $value; // Lets add this field to an array for use later
unset($params[$field]); // Lets unset so we don't bind the param later down the line
}
}
$query_obj = $this->pdo_link->prepare($query);
$query_obj->setFetchMode(PDO::FETCH_ASSOC);
// Now lets bind normal params.
foreach($params as $field => $value) $query_obj->bindValue($field, $value);
// Now lets bind the IN params
foreach($in_fields as $field => $value){
for($i=0,$size=sizeof($value);$i<$size;$i++)
$query_obj->bindValue($field.$i, $value[$i]); // Both the named param index and this index are based off the array index which has not changed...hopefully
}
$query_obj->execute();
if($query_obj->rowCount() <= 0)
return null;
return $query_obj;
}
Это все еще не проверено, однако логика, кажется, там.
Надеюсь, это поможет кому-то в том же положении,
Изменить: После некоторого тестирования я узнал:
- PDO не любит '.' в их именах (что довольно глупо, если вы спросите меня)
- bindParam - неправильная функция, bindValue - правильная функция.
Код отредактирован до рабочей версии.
Небольшое редактирование о коде Schnalle
<?php
$ids = array(1, 2, 3, 7, 8, 9);
$inQuery = implode(',', array_fill(0, count($ids)-1, '?'));
$db = new PDO(...);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN(' . $inQuery . ')'
);
foreach ($ids as $k => $id)
$stmt->bindValue(($k+1), $id);
$stmt->execute();
?>
//implode(',', array_fill(0, count($ids)-1), '?'));
//'?' this should be inside the array_fill
//$stmt->bindValue(($k+1), $in);
// instead of $in, it should be $id
Если столбец может содержать только целые числа, вы, вероятно, могли бы сделать это без заполнителей и просто вставить идентификаторы в запрос напрямую. Вам просто нужно привести все значения массива к целым числам. Как это:
$listOfIds = implode(',',array_map('intval', $ids));
$stmt = $db->prepare(
"SELECT *
FROM table
WHERE id IN($listOfIds)"
);
$stmt->execute();
Это не должно быть уязвимо для любого внедрения SQL.
Какую базу данных вы используете? В PostgreSQL мне нравится использовать ЛЮБОЙ (массив). Итак, чтобы использовать ваш пример:
<?php
$ids=array(1,2,3,7,8,9);
$db = new PDO(...);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id = ANY (:an_array)'
);
$stmt->bindParam('an_array',$ids);
$stmt->execute();
?>
К сожалению, это довольно непереносимо.
В других базах данных вам нужно создать свою магию, как уже упоминали другие. Вы захотите поместить эту логику в класс / функцию, чтобы сделать ее многократно используемой в вашей программе. Посмотрите на комментарии mysql_query
страница на PHP.NET, чтобы узнать больше о предмете и примерах этого сценария.
Как я знаю, нет никакой возможности связать массив в оператор PDO.
Но существует 2 общих решения:
Используйте позиционные заполнители (?,?,?,?) Или именованные заполнители (: id1,: id2,: id3)
$ whereIn = implode (',', array_fill (0, count ($ ids), '?'));
Цитировать массив ранее
$whereIn = array_map(array($db, 'quote'), $ids);
Оба варианта хороши и безопасны. Я предпочитаю второй, потому что он короче, и я могу использовать параметры var_dump, если мне это нужно. Используя заполнители, вы должны связать значения, и в конце ваш код SQL будет таким же.
$sql = "SELECT * FROM table WHERE id IN ($whereIn)";
И последнее и важное для меня - избежать ошибки "количество связанных переменных не соответствует количеству токенов".
Doctrine - это отличный пример использования позиционных заполнителей только потому, что он имеет внутренний контроль над входящими параметрами.
Невозможно использовать такой массив в PDO.
Вам нужно построить строку с параметром (или использовать?) Для каждого значения, например:
:an_array_0, :an_array_1, :an_array_2, :an_array_3, :an_array_4, :an_array_5
Вот пример:
<?php
$ids = array(1,2,3,7,8,9);
$sqlAnArray = join(
', ',
array_map(
function($index) {
return ":an_array_$index";
},
array_keys($ids)
)
);
$db = new PDO(
'mysql:dbname=mydb;host=localhost',
'user',
'passwd'
);
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN('.$sqlAnArray.')'
);
foreach ($ids as $index => $id) {
$stmt->bindValue("an_array_$index", $id);
}
Если вы хотите продолжать использовать bindParam
, вы можете сделать это вместо этого:
foreach ($ids as $index => $id) {
$stmt->bindParam("an_array_$index", $ids[$id]);
}
Если вы хотите использовать ?
заполнители, вы можете сделать это так:
<?php
$ids = array(1,2,3,7,8,9);
$sqlAnArray = '?' . str_repeat(', ?', count($ids)-1);
$db = new PDO(
'mysql:dbname=dbname;host=localhost',
'user',
'passwd'
);
$stmt = $db->prepare(
'SELECT *
FROM phone_number_lookup
WHERE country_code IN('.$sqlAnArray.')'
);
$stmt->execute($ids);
Если вы не знаете, если $ids
пуст, вы должны проверить его и соответственно обработать этот случай (вернуть пустой массив, или вернуть нулевой объект, или выдать исключение, ...).
Пройдя через ту же проблему, я пошел к более простому решению (хотя и не так элегантно, как PDO::PARAM_ARRAY
было бы):
учитывая массив $ids = array(2, 4, 32)
:
$newparams = array();
foreach ($ids as $n => $val){ $newparams[] = ":id_$n"; }
try {
$stmt = $conn->prepare("DELETE FROM $table WHERE ($table.id IN (" . implode(", ",$newparams). "))");
foreach ($ids as $n => $val){
$stmt->bindParam(":id_$n", intval($val), PDO::PARAM_INT);
}
$stmt->execute();
... и так далее
Поэтому, если вы используете массив со смешанными значениями, вам потребуется больше кода для проверки ваших значений перед назначением параметра типа:
// inside second foreach..
$valuevar = (is_float($val) ? floatval($val) : is_int($val) ? intval($val) : is_string($val) ? strval($val) : $val );
$stmt->bindParam(":id_$n", $valuevar, (is_int($val) ? PDO::PARAM_INT : is_string($val) ? PDO::PARAM_STR : NULL ));
Но я не проверял это.
Вот мое решение. Я также расширил класс PDO:
class Db extends PDO
{
/**
* SELECT ... WHERE fieldName IN (:paramName) workaround
*
* @param array $array
* @param string $prefix
*
* @return string
*/
public function CreateArrayBindParamNames(array $array, $prefix = 'id_')
{
$newparams = [];
foreach ($array as $n => $val)
{
$newparams[] = ":".$prefix.$n;
}
return implode(", ", $newparams);
}
/**
* Bind every array element to the proper named parameter
*
* @param PDOStatement $stmt
* @param array $array
* @param string $prefix
*/
public function BindArrayParam(PDOStatement &$stmt, array $array, $prefix = 'id_')
{
foreach($array as $n => $val)
{
$val = intval($val);
$stmt -> bindParam(":".$prefix.$n, $val, PDO::PARAM_INT);
}
}
}
Вот пример использования вышеуказанного кода:
$idList = [1, 2, 3, 4];
$stmt = $this -> db -> prepare("
SELECT
`Name`
FROM
`User`
WHERE
(`ID` IN (".$this -> db -> CreateArrayBindParamNames($idList)."))");
$this -> db -> BindArrayParam($stmt, $idList);
$stmt -> execute();
foreach($stmt as $row)
{
echo $row['Name'];
}
Дайте мне знать, что вы думаете
С MySQL и PDO мы можем использовать массив JSON и
JSON_CONTAINS()
( https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-contains ) для поиска.
$ids = [123, 234, 345, 456]; // Array of users I search
$ids = json_encode($ids); // JSON conversion
$sql = <<<SQL
SELECT ALL user_id, user_login
FROM users
-- Cast is mandatory beaucause JSON_CONTAINS() waits JSON doc candidate
WHERE JSON_CONTAINS(:ids, CAST(user_id AS JSON))
SQL;
$search = $pdo->prepare($sql);
$search->execute([':ids' => $ids]);
$users = $search->fetchAll();
Где также можно использовать
JSON_TABLE()
( https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html#function_json-table ) для более сложных случаев и исследования данных JSON:
$users = [
['id' => 123, 'bday' => ..., 'address' => ...],
['id' => 234, 'bday' => ..., 'address' => ...],
['id' => 345, 'bday' => ..., 'address' => ...],
]; // I'd like to know their login
$users = json_encode($users);
$sql = <<<SQL
SELECT ALL user_id, user_login
FROM users
WHERE user_id IN (
SELECT ALL user_id
FROM JSON_TABLE(:users, '$[*]' COLUMNS (
-- Data exploration...
-- (if needed I can explore really deeply with NESTED kword)
user_id INT PATH '$.id',
-- I could skip these :
user_bday DATE PATH '$.bday',
user_address TINYTEXT PATH '$.address'
)) AS _
)
SQL;
$search = $pdo->prepare($sql);
$search->execute([':users' => $users]);
...
Вы сначала установили номер "?" в запросе, а затем с помощью "для" отправки параметров, как это:
require 'dbConnect.php';
$db=new dbConnect();
$array=[];
array_push($array,'value1');
array_push($array,'value2');
$query="SELECT * FROM sites WHERE kind IN (";
foreach ($array as $field){
$query.="?,";
}
$query=substr($query,0,strlen($query)-1);
$query.=")";
$tbl=$db->connection->prepare($query);
for($i=1;$i<=count($array);$i++)
$tbl->bindParam($i,$array[$i-1],PDO::PARAM_STR);
$tbl->execute();
$row=$tbl->fetchAll(PDO::FETCH_OBJ);
var_dump($row);
Вы можете преобразовать это:
$stmt = $db->prepare('SELECT * FROM table WHERE id IN('.$in.')');
В этом:
$stmt = $db->prepare('SELECT * FROM table WHERE id IN(:id1, :id2, :id3, :id7, :id8, :id9)');
И выполните его с этим массивом:
$stmt->execute(array(
:id1 =>1, :id2 =>2, :id3 =>3, :id7 =>7, :id8 =>8, :id9 => 9
)
);
Таким образом:
$in = array();
$consultaParam = array();
foreach($ids as $k => $v){
$in[] = ':id'.$v;
$consultaParam[':id'.$v] = $v;
}
Окончательный код:
$ids = array(1,2,3,7,8,9);
$db = new PDO(...);
$in = array();
$consultaParam = array();
foreach($ids as $k => $v){
$in[] = ':id'.$v;
$consultaParam[':id'.$v] = $v;
}
$stmt = $db->prepare(
'SELECT *
FROM table
WHERE id IN('.$in.')'
);
$stmt->execute($consultaParam);
Я взял немного дальше, чтобы получить ответ ближе к первоначальному вопросу об использовании заполнителей для связывания параметров.
Этот ответ должен будет сделать два цикла по массиву, который будет использоваться в запросе. Но это решает проблему наличия других заполнителей столбцов для более избирательных запросов.
//builds placeholders to insert in IN()
foreach($array as $key=>$value) {
$in_query = $in_query . ' :val_' . $key . ', ';
}
//gets rid of trailing comma and space
$in_query = substr($in_query, 0, -2);
$stmt = $db->prepare(
"SELECT *
WHERE id IN($in_query)";
//pind params for your placeholders.
foreach ($array as $key=>$value) {
$stmt->bindParam(":val_" . $key, $array[$key])
}
$stmt->execute();