PHP - PDO не принимает взорванный массив в качестве параметра вопросительного знака для предложения IN в запросе SELECT

У меня проблема с параметром вопросительного знака в подготовленном операторе с использованием PDO. Мой класс Query выглядит следующим образом (на данный момент я все еще добавляю такие функции, как ограничения данных, фильтрация пользовательских параметров и автоматическое определение поддерживаемых операторов для используемого драйвера):

// SQL query
class Query {
    public $attributes;

    // constructor for this object
    public function __construct() {
        if ($arguments = func_get_args()) {
            $tmp = explode(" ", current($arguments));

            if (in_array(mb_strtoupper(current($tmp)), ["ALTER", "DELETE", "DROP", "INSERT", "SELECT", "TRUNCATE", "UPDATE"], true)) {
                // classify the query type
                $this->attributes["type"] = mb_strtoupper(current($tmp));

                // get the query string
                $this->attributes["query"] = current($arguments);

                // get the query parameters
                if (sizeof($arguments) > 1) {
                    $this->attributes["parameters"] = array_map(function ($input) { return (is_array($input) ? implode(",", $input) : $input); }, array_slice($arguments, 1, sizeof($arguments)));
                }

                return $this;
            }
        }
    }
}

Это фрагмент кода, который выполняет запрос:

$parameters = (!empty($this->attributes["queries"][$query]->attributes["parameters"]) ? $this->attributes["queries"][$query]->attributes["parameters"] : null);

if ($query = $this->attributes["link"]->prepare($this->attributes["queries"][$query]->attributes["query"], [\PDO::ATTR_CURSOR => \PDO::CURSOR_FWDONLY])) {
    if ($query->execute((!empty($parameters) ? $parameters : null))) {
        return $query->fetchAll(\PDO::FETCH_ASSOC);
    }
}

И вот как я называю это в своем тестовом коде:

$c1->addQuery("lists/product-range", "SELECT * FROM `oc_product` WHERE `product_id` IN (?);", [28, 29, 30, 46, 47]);

if ($products = $c1->execute("test2")) {
    foreach ($products as $product) {
        print_r($product);
    }
}

У меня проблема в том, что я просто вижу первый продукт (это тест против установки vanilla OpenCart) с идентификатором 28. Как вы можете видеть в моем коде, если переданный параметр является массивом, он автоматически определяется лямбдой, которую я имею на месте в конструкторе класса Query, поэтому он отображается как строка, например 28,29,30,46,47.

Есть ли отсутствующий параметр в настройке PDO, которого мне не хватает? Или, может быть, в том, что я делаю, есть какая-то ошибка или ограничение платформы? Я знаю, что есть некоторые ограничения на то, что PDO может делать в отношении массивов, и поэтому я предварительно разбиваю все массивы, чтобы они передавались как простая строка.

Есть несколько процедур, которые я видел здесь в SO, которые, в основном, составляют строку запроса, такую ​​как WHERE product_id IN ({$marks}), где $marks динамически генерируется с использованием процедуры, такой как str_repeat("?", sizeof($parameters)), но это не то, что я ищу (я мог бы прибегнуть к этому, если есть нет известной альтернативы, но это не выглядит очень элегантным решением).

Моя среда разработки состоит из: Windows 7 x64, PHP 5.4.13 (x86, потокобезопасный), Apache 2.4.4 (x86) и MySQL 5.6.10 x64.

Любая подсказка будет принята с благодарностью :)


person Julio María Meca Hansen    schedule 07.04.2013    source источник
comment
Ну, я не вижу, где что-то выполняется. Во-первых, экземпляр Query никогда не создается. Во-вторых: эти фрагменты кода внутри Query или снаружи? Почему используется так много массивов - я имею в виду, что это должно быть ООП, не так ли, используйте его, реализуя свойства. И я понятия не имею, почему функция setLimit отображается, но также никогда не используется.   -  person Sven    schedule 07.04.2013
comment
Извините, я не хотел копировать метод setLimit(). Забыл почистить. Упомянутые вами свойства реализованы с помощью свойства attribute, которое, по сути, является ассоциативным массивом. Хорошо служит цели и позволяет избежать пустых элементов, когда какое-либо свойство не применимо или не включено во время строительства. Класс Query завершен сам по себе. Существует четыре класса для управления базой данных: Manager, Connection, Query и QueryGroup. В данном случае фрагмент выполнения принадлежит классу Connection (поэтому он может управлять как одиночными, так и групповыми запросами).   -  person Julio María Meca Hansen    schedule 08.04.2013
comment
Хорошо, тогда отредактируйте свой вопрос. И когда я убираю мысленно функцию setLimit, я действительно не знаю, почему вообще упоминается класс Query, потому что я все еще не вижу, где используется конструктор или его экземпляр.   -  person Sven    schedule 08.04.2013


Ответы (2)


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

Например, если вы хотите выбрать идентификаторы в массиве [1, 2], вам нужен запрос, который выглядит как SELECT * FROM tbl WHERE id IN (?,?). Если вы затем передаете массив из трех элементов, вам нужно подготовить запрос, например SELECT * FROM tbl WHERE id IN (?,?,?), и так далее.

Другими словами, вы не можете с уверенностью знать, какой запрос вы хотите построить/создать, до тех пор, пока у вас не появятся данные, которые вы хотите привязать к подготовленному оператору.

Это не ограничение PDO, это основа работы подготовленных запросов в базах данных SQL. Подумайте об этом — каким типом данных будет ? в мире SQL, если вы скажете IN ?, но ? заменит что-то нескалярное?

Некоторые базы данных имеют типы массивов (например, PostgreSQL). Возможно они могут интерпретировать IN <array-type> так же, как IN (?,?,...), и это сработает. Но PDO не может отправлять или данные в виде массива (нет PDO::PARAM_ARRAY), и, поскольку это необычная и эзотерическая функция, маловероятно, что PDO когда-либо будет.

Обратите внимание, что здесь есть дополнительный слой поломки. Обычная база данных, столкнувшись с условием int_id = '1,2,3,4', ничего не найдет, поскольку '1,2,3,4' нельзя привести к целому числу. Однако MySQL преобразует это в целое число 1! Вот почему ваш запрос:

$pstmt = $db->prepare('SELECT * FROM `oc_product` WHERE `product_id` IN (?)');
$pstmt->execute(array('28,29,30,46,47'));

Будет соответствовать product_id = 28. Узрите безумие:

mysql> SELECT CAST('28,29,30,46,47' AS SIGNED INTEGER);
+------------------------------------------+
| CAST('28,29,30,46,47' AS SIGNED INTEGER) |
+------------------------------------------+
| 28                                       |
+------------------------------------------+
1 rows in set (0.02 sec)
person Francis Avila    schedule 07.04.2013
comment
Хорошо, я понимаю вашу точку зрения. все еще... Я не понимаю, почему PDO не обрабатывает мою строку как одну переменную. Ваша точка зрения выглядит вполне обоснованной, если вы передаете [1, 2] как массив методу execute(), но я передаю массив, в котором единственная позиция (0) представляет собой строку, содержащую 28,29,30,46,47. Так что, если я это понимаю, это не имеет значения. Я должен выполнить некоторую предварительную обработку строки запроса и заменить этот единственный вопросительный знак тем количеством параметров, которое содержится в переданной строке? - person Julio María Meca Hansen; 08.04.2013
comment
Вы неправильно понимаете, что такое привязка запроса. id IN (1,2) совпадает с id = 1 OR id = 2. То, что вы делаете, это id IN ('1,2') (одна строка), которая совпадает с id = '1,2' (т.е. ID равен строке '1,2'), что очень не то, что вы намеревались!! - person Francis Avila; 08.04.2013
comment
Другими словами, column IN (?) всегда бессмысленно — оно означает то же, что и column = ?. IN используется, когда у вас есть список значений, и только одно из них должно совпадать. Поскольку ? заменяет только литералы, вам нужно по одному ? на элемент в списке. - person Francis Avila; 08.04.2013
comment
Хорошо, теперь я это понимаю. Подстановка делает мою строку похожей на «...», независимо от ее размера. Спасибо за объяснение :) - person Julio María Meca Hansen; 08.04.2013
comment
@JulioMecaHansen, а также MySQL сбивает с толку из-за своих правил приведения типов. При сравнении строки с целым числом '1,2' преобразуется в целое число 1, поэтому вы получаете одно совпадение, а не ни одного. (См. вывод SELECT CAST('1,4' AS SIGNED); это безумие и лишь малая часть того, почему MySQL и PHP должны быть вместе во фрактале плохого дизайна. - person Francis Avila; 08.04.2013
comment
Нашел решение. Я думаю, что это не идеальный вариант, но он отлично справляется со своей задачей... он сканирует каждый вопросительный знак, разбивает строку запроса, повторно адаптирует массив параметров, а затем повторно составляет строку запроса до ее окончательной формы. Большое спасибо за ценные советы :) - person Julio María Meca Hansen; 08.04.2013
comment
'('.implode(',',array_fill(0,sizeof($myarray),'?')).')'; - person ladieu; 25.02.2014

Lambda обнаруживает массив и создает из него строку с разделителями-запятыми, а переданный аргумент обрабатывается как строка, поэтому запрос выглядит так:

SELECT * FROM tbl WHERE id IN('1,2,3,4')

'1,2,3,4' — это одно строковое значение для SQL.

Если вы ожидаете только числовые значения, вы можете не добавлять их в качестве параметров и просто указать в запросе:

$a = [28, 29, 30, 46, 47];
$s = "SELECT * FROM tbl WHERE id IN(".implode(',', array_map('intval', $a)).")";

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

person dev-null-dweller    schedule 07.04.2013
comment
Класс Query вызывается из класса Connection (который управляет подключением к базе данных) с помощью метода addQuery(), в котором вы предоставляете те же аргументы, что и функции sprintf(). Сначала строка запроса, а затем столько параметров, сколько вам нужно указать. Вот почему составление строки запроса «вручную» возможно, но не то, что я ищу. - person Julio María Meca Hansen; 08.04.2013