Могу ли я привязать массив к условию 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(); ?> 

Что, безусловно, выполняет эту работу, но просто интересно, есть ли встроенное решение, которое мне не хватает?

Я думаю, что soulmerge прав. вам придется построить строку запроса.

 <?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(); ?> 

Исправьте: да, вы были правы. исправил код (не тестировал его)

edit: оба chris (комментарии) и somebodyisintrouble предположили, что foreach-loop …

 (...) // 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 op.

Например, в 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: Как указывали некоторые люди в комментариях к этому ответу, есть некоторые вопросы, которые следует изложить всерьез.

  1. FIND_IN_SET не использует индекс в таблице, и он все еще не реализован – см. Эту запись в отслеживателе ошибок MYSQL . Спасибо @BillKarwin за уведомление.
  2. Вы не можете использовать строку с запятой внутри как значение массива для поиска. Невозможно разобрать такую ​​строку в правильном направлении после 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 привязки $bindArray которая вам понадобится, когда придет время для запуска вашего запроса. Легко!

Решение от 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(); 

Я расширил 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(); 

Вот мое решение:

 $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); 

И это было неудачно.

очень чистый способ для 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)); 

Я также понимаю, что эта ветка устарела, но у меня была уникальная проблема, когда при преобразовании скоро устаревшего драйвера mysql в драйвер PDO мне пришлось создать функцию, которая могла бы динамически строить как обычные параметры, так и INs из того же param. Поэтому я быстро построил это:

 /** * 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; } не /** * 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 – это правильная функция.

Код отредактирован до рабочей версии.

Глядя на PDO: предопределенные константы, нет PDO :: PARAM_ARRAY, которые вам понадобятся, как указано в PDOStatement-> bindParam

bool PDOStatement :: bindParam (смешанный параметр $, mixed & $ variable [, int $ data_type [, int $ length [, mixed $ driver_options]]])

Поэтому я не думаю, что это достижимо.

Небольшое редактирование кода 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 

Если у вас есть другой параметр, вы можете сделать следующее:

 $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(); 

Какую базу данных вы используете? В PostgreSQL мне нравится использовать ANY (массив). Поэтому для повторного использования вашего примера:

 <?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::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(); 

… и так далее

Поэтому, если вы используете массив смешанных значений, вам потребуется больше кода для проверки ваших значений перед назначением типа param:

 // 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.

Но существует 2 общих решения:

  1. Используйте Placeal Placeholders (?,?,?,?) Или Named Placeholders (: id1,: id2,: id3)

    $ whereIn = implode (',', array_fill (0, count ($ ids), '?'));

  2. Quote массив ранее

    $ whereIn = array_map (массив ($ db, 'quote'), $ ids);

Оба варианта хороши и безопасны. Я предпочитаю второй, потому что он короче, и я могу var_dump параметры, если мне это нужно. Используя заполнители, вы должны привязать значения, и в конце ваш код SQL будет таким же.

 $sql = "SELECT * FROM table WHERE id IN ($whereIn)"; 

И последнее и важное для меня исключение ошибки «количество связанных переменных не совпадает с числом токенов»

Doctrine – отличный пример использования позиционных заполнителей, только потому, что он имеет внутренний контроль над входящими параметрами.

Если столбец может содержать только целые числа, вы могли бы сделать это без заполнителей и просто поместить идентификаторы в запрос напрямую. Вам просто нужно передать все значения массива целым числам. Как это:

 $listOfIds = implode(',',array_map('intval', $ids)); $stmt = $db->prepare( "SELECT * FROM table WHERE id IN($listOfIds)" ); $stmt->execute(); 

Это не должно быть уязвимым для любой SQL-инъекции.

вот мое решение. Я также расширил класс 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']; } 

Дайте мне знать, что вы думаете

Для меня более сексуальное решение – построить динамический ассоциативный массив и использовать его

 // 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); 

Я взял его немного дальше, чтобы получить ответ ближе к исходному вопросу об использовании заполнителей для привязки параметров.

Этот ответ должен будет сделать два цикла через массив, который будет использоваться в запросе. Но он решает проблему наличия других заполнителей столбцов для более выборочных запросов.

 //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(); 

вы сначала установили число «?» в запросе, а затем с помощью параметров «для» send:

 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);