Подготовленные операторы MySQL со списком переменных переменного размера

Как бы вы написали подготовленный оператор MySQL на PHP, который каждый раз принимает разное количество аргументов? Пример такого запроса:

SELECT `age`, `name` FROM `people` WHERE id IN (12, 45, 65, 33)

Предложение IN будет иметь разное количество id при каждом запуске.

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

Возможное решение 1. Сделайте так, чтобы оператор принимал 100 переменных, а остальные заполняли фиктивными значениями, которых гарантированно не было в таблице; сделать несколько вызовов для более чем 100 значений.

Возможное решение 2 Не используйте подготовленный оператор; построить и запустить запрос, строго проверяя возможные атаки с использованием инъекций.


person smarthall    schedule 29.11.2008    source источник
comment
Я подозреваю, что я бы избегал обоих предлагаемых вами решений. Подготовленные операторы значительно упрощают защиту вашего кода. И ваше первое решение кажется довольно неэффективным и расточительным, если у вас есть небольшой список.   -  person Zoredache    schedule 29.11.2008
comment
Я согласен, я думал, что эти идеи могут стимулировать умы других. :)   -  person smarthall    schedule 29.11.2008
comment
Для SQL Server см. Параметризация предложения SQL IN?   -  person outis    schedule 04.04.2012


Ответы (7)


Я могу придумать пару решений.

Одним из решений может быть создание временной таблицы. Сделайте вставку в таблицу для каждого параметра, который у вас будет в предложении 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);

person Zoredache    schedule 29.11.2008
comment
мне нравится ваше второе предложение. сделайте это и забудьте об этом, пока производительность не станет проблемой. в этот момент, возможно, стоит изучить первый вариант. - person benlumley; 29.11.2008
comment
Если бы я только подумал об этом! Ваше первое решение похоже на то, что я искал. - person smarthall; 29.11.2008
comment
Я часто использовал схему №2. DBI Perl имеет функцию prepare_cached (), поэтому, если вы запрашиваете аналогичное количество заполнителей, он будет повторно использовать дескрипторы операторов. Хотя насчет PHP не уверен ... - person Gary Richardson; 30.11.2008
comment
Создание временной таблицы было бы излишним, если набор данных не огромен. Пример кода слишком сложен (половину этих переменных можно исключить, а код можно значительно упростить, выполнив большую часть работы в режиме реального времени). Но основная идея хороша, используя implode (',', array_fill (0, count ($ params), '?')) Для генерации? S, а затем просто передайте $ params в качестве данных для привязки. - person orrd; 15.01.2014
comment
@orrd, meh ... Когда я писал этот ответ, я пытался сделать его подробным, чтобы его было легко понять. Я согласен с тем, что его можно упростить, но я не думаю, что предложенное вами упрощение упростит понимание ответа или окажет какое-либо существенное влияние на производительность. В любом случае я обновлю свой ответ и добавлю для вас компактную версию. - person Zoredache; 15.01.2014
comment
Спустя столько лет нет лучшего способа, чем обойти это? Почему он не принимает массив ... Насколько я видел, call_user_func_array по-прежнему является наиболее "менее хакерским" способом сделать это ... - person Kerwin Sneijders; 09.09.2018

Также существует FIND_IN_SET функция, второй параметр которой строка значений, разделенных запятыми:

SELECT age, name FROM people WHERE FIND_IN_SET(id, '12,45,65,33')
person Gumbo    schedule 10.06.2012
comment
Проблема в том, что он не будет использовать первичный индекс, по умолчанию используется полное сканирование таблицы при выполнении FIND_IN_SET для каждой строки. - person Hugo Maxwell; 26.01.2015

достойные оболочки sql поддерживают привязку к значениям массива. т.е.

$sql = "... WHERE id IN (?)";
$values = array(1, 2, 3, 4);
$result = $dbw -> prepare ($sql, $values) -> execute ();
person Eimantas    schedule 29.11.2008
comment
На самом деле я не знаю ни одной собственной библиотеки доступа к базе данных PHP для MySQL (ни mysql, ни mysqli, ни PDO), которая позволяет связывать параметры типа массива. - person Stefan Gehrig; 29.11.2008
comment
Когда несколько лет назад я разрабатывал php, adodb мне очень помог. Я думаю, тебе стоит это проверить. - person Eimantas; 29.11.2008
comment
Любая структура, которая делает это, делает это, расширяя список и интерполируя его в запрос SQL перед подготовкой (). Это не то же самое, что и связанные параметры. - person Bill Karwin; 29.11.2008

Пожалуйста, снимите №2 со стола. Подготовленные операторы - единственный способ защитить себя от SQL-инъекции.

Однако вы можете создать динамический набор переменных привязки. т.е. не делайте 100, если вам нужно 7 (или 103).

person Dustin    schedule 29.11.2008
comment
какие? это не имеет смысла. Он использует подготовленные операторы, но динамически устанавливает количество заполнителей. - person Gary Richardson; 30.11.2008
comment
В сценарии № 1 он статически определял запрос, принимающий 100 параметров, в сценарии № 2 он не использовал подготовленный оператор. Мое предложение заключалось в динамическом построении запроса с привязками, о чем вы говорите. - person Dustin; 30.11.2008
comment
упс. Я читал № 2 из stackoverflow.com/questions/327274/. Извините! - person Gary Richardson; 02.12.2008

я получил свой ответ от: http://bugs.php.net/bug.php?id=43568 - это мое рабочее решение моей проблемы. Теперь я могу динамически использовать столько параметров, сколько хочу. Они будут тем же номером, что и у меня в массиве, или как в этом случае я передаю идентификаторы из последнего запроса (который нашел все идентификаторы, где email = '[email protected]') в динамический запрос, чтобы получить все информация о каждом из этих идентификаторов независимо от того, сколько мне понадобится.

<?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();
?>
person Community    schedule 18.01.2009

Если вы используете только целочисленные значения в предложении 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)

Но, без сомнения, решение здесь - это более общий подход к этой проблеме.

person Stefan Gehrig    schedule 29.11.2008
comment
Почему решение того, как сделать X с подготовленным оператором? динамически построить другой запрос? Если вы используете подготовленные операторы для повторного использования кэшированных планов запросов, значит, вы это подорвали. Если вы делаете это, чтобы предотвратить SQL-инъекцию, ну, это другое. - person Brandon; 11.07.2014

У меня была похожая проблема сегодня, и я нашел эту тему. Глядя на ответы и ища в Google, я нашел красивое решение.

Хотя у меня проблема немного сложнее. Потому что у меня фиксированные значения привязки и динамические тоже.

Это решение.

$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 добавляет один массив в другой (не array_push или +), [ PHP]: Ошибка - ›Слишком мало аргументов в sprintf ();, http://no2.php.net/manual/en/mysqli-stmt.bind-param.php#89171, Проблема передачи по ссылке в PHP 5.3.1

person user3065191    schedule 17.01.2014