web-dev-qa-db-fra.com

Puis-je lier un tableau à une condition IN ()?

Je suis curieux de savoir s'il est possible de lier un tableau de valeurs à un espace réservé à l'aide de PDO. Le cas d'utilisation ici tente de transmettre un tableau de valeurs à utiliser avec une condition IN().

J'aimerais pouvoir faire quelque chose comme ça:

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

Et associez PDO et citez toutes les valeurs du tableau.

En ce moment je fais:

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

Lequel fait certainement le travail, mais se demandant simplement s'il y a une solution intégrée qui me manque?

544
Andru

je pense que soulmerge a raison. vous devrez construire la chaîne de requête.

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

correctif: dan, tu avais raison. corrigé le code (mais ne l'a pas testé)

edit: chris (commentaires) et somebodyisintrouble ont suggéré que la boucle foreach ...

(...)
// bindvalue is 1-indexed, so $k+1
foreach ($ids as $k => $id)
    $stmt->bindValue(($k+1), $id);

$stmt->execute();

... peut être redondant, la boucle foreach et le $stmt->execute pourraient donc être remplacés par ...

<?php 
  (...)
  $stmt->execute($ids);
?>

(encore une fois, je ne l'ai pas testé)

250
stefs

Pour quelque chose de rapide:

//$db = new PDO(...);
//$ids = array(...);

$qMarks = str_repeat('?,', count($ids) - 1) . '?';
$sth = $db->prepare("SELECT * FROM myTable WHERE id IN ($qMarks)");
$sth->execute($ids);
167
DonVaughn

Est-il si important d'utiliser l'instruction IN? Essayez d'utiliser FIND_IN_SET op.

Par exemple, il existe une requête dans PDO comme celle-ci

SELECT * FROM table WHERE FIND_IN_SET(id, :array)

Ensuite, il vous suffit de lier un tableau de valeurs, implosé avec une virgule, comme celui-ci

$ids_string = implode(',', $array_of_smth); // WITHOUT WHITESPACES BEFORE AND AFTER THE COMMA
$stmt->bindParam('array', $ids_string);

et c'est fait.

UPD: Comme certains l’ont souligné dans les commentaires sur cette réponse, certaines questions doivent être énoncées explicitement.

  1. FIND_IN_SET n'utilise pas d'index dans une table et n'est pas encore implémenté - voir cet enregistrement dans le suivi des bogues MYSQL . Merci à @ BillKarwin pour la notification.
  2. Vous ne pouvez pas utiliser une chaîne avec une virgule à l'intérieur comme valeur du tableau pour la recherche. Il est impossible d'analyser correctement cette chaîne après implode puisque vous utilisez le symbole de virgule comme séparateur. Merci à @VaL pour la note.

En résumé, si vous ne dépendez pas beaucoup des index et n'utilisez pas de chaînes avec des virgules pour la recherche, ma solution sera beaucoup plus facile, plus simple et plus rapide que les solutions répertoriées ci-dessus.

46

Comme je fais beaucoup de requêtes dynamiques, c’est une fonction d’aide extrêmement simple que j’ai réalisée.

public static function bindParamArray($prefix, $values, &$bindArray)
{
    $str = "";
    foreach($values as $index => $value){
        $str .= ":".$prefix.$index.",";
        $bindArray[$prefix.$index] = $value;
    }
    return rtrim($str,",");     
}

Utilisez-le comme ceci:

$bindString = helper::bindParamArray("id", $_GET['ids'], $bindArray);
$userConditions .= " AND users.id IN($bindString)";

Renvoie une chaîne :id1,:id2,:id3 et met également à jour votre $bindArray des liaisons dont vous aurez besoin au moment d'exécuter votre requête. Facile!

32
prograhammer

manière très propre pour postgres utilise le postgres-array ("{}"):

$ids = array(1,4,7,9,45);
$param = "{".implode(', ',$ids)."}";
$cmd = $db->prepare("SELECT * FROM table WHERE id = ANY (?)");
$result = $cmd->execute(array($param));
17
ESCOBAR

La solution d'EvilRygy n'a pas fonctionné pour moi. Dans Postgres, vous pouvez effectuer une autre solution de contournement:


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

J'ai étendu AOP à faire quelque chose de similaire à ce que suggère stefs, et cela a été plus facile pour moi à long terme:

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

Vous pouvez l'utiliser comme ceci:

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

Voici ma solution:

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

Notez l'utilisation de array_values. Cela peut résoudre les problèmes de classement des clés.

Je fusionnais des tableaux d'identifiants puis retirais les éléments en double. J'ai eu quelque chose comme:

$ids = array(0 => 23, 1 => 47, 3 => 17);

Et c'était en train d'échouer.

12
Progrock

Lorsque vous avez un autre paramètre, vous pouvez faire comme ceci:

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

Pour moi, la solution la plus sexy est de construire un tableau associatif dynamique et de l'utiliser

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

En regardant PDO: constantes prédéfinies il n'y a pas de PDO :: PARAM_ARRAY dont vous auriez besoin comme indiqué sur la liste PDOStatement-> bindParam

bool PDOStatement :: bindParam (paramètre mixte $, mixte & variable [ int $ type_données [ entier $ longueur [ options mixtes $ driver_options]]] )

Donc, je ne pense pas que ce soit réalisable.

10
user83632

Je me rends également compte que ce fil est ancien, mais j’ai eu un problème unique: lors de la conversion du pilote mysql obsolète bientôt obsolète en pilote PDO, j’ai dû créer une fonction permettant de créer, de manière dynamique, à la fois les paramètres normaux et les IN. param tableau. Alors j'ai vite construit ceci:

/**
 * 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;
}

Il n'a toujours pas été testé, mais la logique semble être là.

J'espère que ça aide quelqu'un dans la même position,

Edit: Après quelques tests, j'ai découvert:

  • PDO n'aime pas '.' dans leurs noms (ce qui est un peu stupide si vous me demandez)
  • bindParam est la mauvaise fonction, bindValue est la bonne fonction.

Code édité en version de travail.

9
Sammaye

Une petite retouche sur le code de 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
8
Aaron Angelo Vicuna

Quelle base de données utilisez-vous? Dans PostgreSQL, j'aime utiliser ANY (tableau). Donc, pour réutiliser votre exemple:

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

Malheureusement, ce n'est pas assez portable.

Sur d'autres bases de données, vous devrez créer votre propre magie, comme d'autres l'ont mentionné. Vous voudrez mettre cette logique dans une classe/fonction pour la rendre réutilisable tout au long de votre programme. Consultez les commentaires sur la page mysql_query sur PHP.NET pour obtenir plus d'informations sur le sujet et des exemples de ce scénario.

7
Ryan Bair

Après avoir traversé le même problème, je suis allé à une solution plus simple (bien que toujours pas aussi élégante qu'un PDO::PARAM_ARRAY serait):

étant donné le tableau $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();

... etc

Donc, si vous utilisez un tableau de valeurs mélangées, vous aurez besoin de plus de code pour tester vos valeurs avant d'affecter le type 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 ));

Mais je n'ai pas testé celui-ci.

4
alan_mm

Comme je le sais, il n’existe aucune possibilité de lier un tableau à une instruction PDO.

Mais existe 2 solutions communes:

  1. Utiliser des espaces réservés de position (?,?,?,?) Ou des espaces réservés nommés (: id1,: id2,: id3)

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

  2. Citer le tableau plus tôt

    $ whereIn = array_map (array ($ db, 'quote'), $ ids);

Les deux options sont bonnes et sûres. Je préfère le second parce que c'est plus court et que je peux var_dump en paramètre si j'en ai besoin. En utilisant des espaces réservés, vous devez lier des valeurs et votre code SQL sera finalement le même.

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

Et le dernier et important pour moi est d'éviter l'erreur "le nombre de variables liées ne correspond pas au nombre de jetons"

Doctrine est un excellent exemple d'utilisation d'espaces réservés de position, uniquement parce qu'elle dispose d'un contrôle interne sur les paramètres entrants.

4
Oleg Matei

Si la colonne ne peut contenir que des entiers, vous pouvez probablement le faire sans espaces réservés et mettre les identifiants directement dans la requête. Il vous suffit de convertir toutes les valeurs du tableau en entiers. Comme ça:

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

Cela ne devrait pas être vulnérable à une injection SQL.

4
Kodos Johnson

voici ma solution. J'ai également étendu la classe 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);
        }
    }
}

Voici un exemple d'utilisation du code ci-dessus:

$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'];
}

Laissez-moi savoir ce que vous pensez

3
Lippai Zoltan

Il n'est pas possible d'utiliser un tableau comme celui-ci dans PDO.

Vous devez créer une chaîne avec un paramètre (ou utiliser?) Pour chaque valeur, par exemple:

:an_array_0, :an_array_1, :an_array_2, :an_array_3, :an_array_4, :an_array_5

Voici un exemple:

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

Si vous voulez continuer à utiliser bindParam, vous pouvez le faire à la place:

foreach ($ids as $index => $id) {
    $stmt->bindParam("an_array_$index", $ids[$id]);
}

Si vous voulez utiliser les espaces réservés ?, vous pouvez le faire comme ceci:

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

Si vous ne savez pas si $ids est vide, vous devez le tester et gérer ce cas en conséquence (renvoyer un tableau vide, ou renvoyer un objet Null, ou lever une exception, ...).

1
Pedro Amaral Couto

Je suis allé un peu plus loin pour rapprocher la réponse de la question initiale consistant à utiliser des espaces réservés pour lier les paramètres.

Cette réponse devra faire deux boucles à travers le tableau pour être utilisé dans la requête. Mais cela résout le problème d'avoir d'autres espaces réservés de colonne pour des requêtes plus sélectives.

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

vous avez d'abord défini le nombre de "?" dans la requête et ensuite par un "pour" envoyer des paramètres comme ceci:

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);
0
Ali Chegini