PDO Prepared Insère plusieurs lignes dans une seule requête

146

J'utilise actuellement ce type de SQL sur MySQL pour insérer plusieurs lignes de valeurs dans une seule requête:

INSERT INTO `tbl` (`key1`,`key2`) VALUES ('r1v1','r1v2'),('r2v1','r2v2'),...

Sur les lectures sur PDO, l'utilisation des instructions préparées devrait me donner une meilleure sécurité que les requêtes statiques.

Je voudrais donc savoir s'il est possible de générer "l'insertion de plusieurs lignes de valeurs par l'utilisation d'une seule requête" à l'aide d'instructions préparées.

Si oui, puis-je savoir comment puis-je le mettre en œuvre?

Hoball
la source
attention avec beaucoup de réponses pour $stmt->execute($data); php.net/manual/en/... Fondamentalement, tous les paramètres sont passés validés sous forme de chaînes. Il suffit de parcourir les données après la création de la requête, et manuellement bindValueou en bindParampassant le type comme troisième argument.
MrMesees

Réponses:

151

Insertion de valeurs multiples avec instructions préparées par PDO

Insertion de plusieurs valeurs dans une instruction d'exécution. Pourquoi parce que selon cette page, il est plus rapide que les insertions régulières.

$datafields = array('fielda', 'fieldb', ... );

$data[] = array('fielda' => 'value', 'fieldb' => 'value' ....);
$data[] = array('fielda' => 'value', 'fieldb' => 'value' ....);

plus de valeurs de données ou vous avez probablement une boucle qui remplit les données.

Avec les insertions préparées, vous devez connaître les champs dans lesquels vous insérez et le nombre de champs pour créer le? des espaces réservés pour lier vos paramètres.

insert into table (fielda, fieldb, ... ) values (?,?...), (?,?...)....

C'est essentiellement à quoi nous voulons que l'instruction insert ressemble.

Maintenant, le code:

function placeholders($text, $count=0, $separator=","){
    $result = array();
    if($count > 0){
        for($x=0; $x<$count; $x++){
            $result[] = $text;
        }
    }

    return implode($separator, $result);
}

$pdo->beginTransaction(); // also helps speed up your inserts.
$insert_values = array();
foreach($data as $d){
    $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
    $insert_values = array_merge($insert_values, array_values($d));
}

$sql = "INSERT INTO table (" . implode(",", $datafields ) . ") VALUES " .
       implode(',', $question_marks);

$stmt = $pdo->prepare ($sql);
try {
    $stmt->execute($insert_values);
} catch (PDOException $e){
    echo $e->getMessage();
}
$pdo->commit();

Bien que dans mon test, il n'y avait qu'une différence de 1 seconde lors de l'utilisation de plusieurs inserts et d'inserts préparés réguliers avec une seule valeur.

Herbert Balagtas
la source
4
Une faute de frappe, dans l'explication ci-dessus, elle mentionne $ datafields bien que $ datafield soit utilisé dans $ sql. Ainsi, le copier-coller entraînerait une erreur. Veuillez rectifier. Merci pour cette solution.
pal4life
1
Je l'ai utilisé pendant un moment, puis j'ai remarqué que les valeurs avec des guillemets simples ne sont pas correctement échappées. Utiliser des guillemets doubles sur l'implosion fonctionne comme un charme pour moi: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman
1
array_merge semble plus cher que d'utiliser simplement un array_push.
K2xL
14
Quand vous dites "il n'y avait qu'une différence de 1 seconde", combien de lignes une donnée insérez-vous? 1 seconde est assez significative selon le contexte.
Kevin Dice
3
Optimisation: inutile d'appeler placeholders()encore et encore. Appelez-le une fois avant la boucle avec sizeof($datafields)et ajoutez la chaîne de résultat à l' $question_marks[]intérieur de la boucle.
AVIDeveloper
71

Même réponse que M. Balagtas, un peu plus claire ...

Les versions récentes de MySQL et PHP PDO prennent en charge les INSERTinstructions multi-lignes .

Présentation de SQL

Le SQL ressemblera à quelque chose comme ceci, en supposant une table à 3 colonnes que vous souhaitez INSERT.

INSERT INTO tbl_name
            (colA, colB, colC)
     VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) [,...]

ON DUPLICATE KEY UPDATEfonctionne comme prévu même avec un INSERT à plusieurs lignes; ajoutez ceci:

ON DUPLICATE KEY UPDATE colA = VALUES(colA), colB = VALUES(colB), colC = VALUES(colC)

Présentation de PHP

Votre code PHP suivra les appels habituels $pdo->prepare($qry)et $stmt->execute($params)PDO.

$paramssera un tableau unidimensionnel de toutes les valeurs à passer au INSERT.

Dans l'exemple ci-dessus, il doit contenir 9 éléments; PDO utilisera chaque ensemble de 3 comme une seule ligne de valeurs. (Insertion de 3 lignes de 3 colonnes chacune = tableau de 9 éléments.)

la mise en oeuvre

Le code ci-dessous est écrit pour plus de clarté, pas d'efficacité. Travaillez avec les array_*()fonctions PHP pour de meilleures façons de mapper ou de parcourir vos données si vous le souhaitez. La possibilité d'utiliser des transactions dépend évidemment de votre type de table MySQL.

En supposant:

  • $tblName - le nom de chaîne de la table à INSÉRER
  • $colNames- Tableau unidimensionnel des noms de colonnes de la table Ces noms de colonnes doivent être des identifiants de colonne MySQL valides; échappez-leur avec des backticks (``) s'ils ne le sont pas
  • $dataVals - tableau multidimensionnel, où chaque élément est un tableau 1-d d'une ligne de valeurs à INSÉRER

Exemple de code

// setup data values for PDO
// memory warning: this is creating a copy all of $dataVals
$dataToInsert = array();

foreach ($dataVals as $row => $data) {
    foreach($data as $val) {
        $dataToInsert[] = $val;
    }
}

// (optional) setup the ON DUPLICATE column names
$updateCols = array();

foreach ($colNames as $curCol) {
    $updateCols[] = $curCol . " = VALUES($curCol)";
}

$onDup = implode(', ', $updateCols);

// setup the placeholders - a fancy way to make the long "(?, ?, ?)..." string
$rowPlaces = '(' . implode(', ', array_fill(0, count($colNames), '?')) . ')';
$allPlaces = implode(', ', array_fill(0, count($dataVals), $rowPlaces));

$sql = "INSERT INTO $tblName (" . implode(', ', $colNames) . 
    ") VALUES " . $allPlaces . " ON DUPLICATE KEY UPDATE $onDup";

// and then the PHP PDO boilerplate
$stmt = $pdo->prepare ($sql);

try {
   $stmt->execute($dataToInsert);
} catch (PDOException $e){
   echo $e->getMessage();
}

$pdo->commit();
jamesvl
la source
6
C'est vraiment dommage que PDO le gère de cette façon, il existe des moyens très élégants de le faire dans d'autres pilotes de base de données.
Jonathon
Cela configure les espaces réservés de manière encore plus laconique, ce qui $rowPlacesne rend plus nécessaire:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil
Fonctionne parfaitement. J'ajouterais à cette réponse la nécessité d'assurer l'unicité des (combinaisons) d'index dans le tableau. Comme dans ALTER TABLE votesADD UNIQUE unique_index( user, email, address);
Giuseppe
1
Impressionnant! BTW, l'utilisation array_push($dataToInsert, ...array_values($dataVals));sera beaucoup plus rapide alorsforeach ($dataVals as $row => $data) {}
Anis
39

Pour ce que cela vaut, j'ai vu de nombreux utilisateurs recommander d'itérer les instructions INSERT au lieu de créer une requête de chaîne unique comme le faisait la réponse sélectionnée. J'ai décidé de lancer un test simple avec seulement deux champs et une instruction d'insertion très basique:

<?php
require('conn.php');

$fname = 'J';
$lname = 'M';

$time_start = microtime(true);
$stmt = $db->prepare('INSERT INTO table (FirstName, LastName) VALUES (:fname, :lname)');

for($i = 1; $i <= 10; $i++ )  {
    $stmt->bindParam(':fname', $fname);
    $stmt->bindParam(':lname', $lname);
    $stmt->execute();

    $fname .= 'O';
    $lname .= 'A';
}


$time_end = microtime(true);
$time = $time_end - $time_start;

echo "Completed in ". $time ." seconds <hr>";

$fname2 = 'J';
$lname2 = 'M';

$time_start2 = microtime(true);
$qry = 'INSERT INTO table (FirstName, LastName) VALUES ';
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?), ";
$qry .= "(?,?)";

$stmt2 = $db->prepare($qry);
$values = array();

for($j = 1; $j<=10; $j++) {
    $values2 = array($fname2, $lname2);
    $values = array_merge($values,$values2);

    $fname2 .= 'O';
    $lname2 .= 'A';
}

$stmt2->execute($values);

$time_end2 = microtime(true);
$time2 = $time_end2 - $time_start2;

echo "Completed in ". $time2 ." seconds <hr>";
?>

Alors que la requête globale elle-même prenait des millisecondes ou moins, la dernière requête (chaîne unique) était systématiquement 8 fois plus rapide ou plus. Si cela était conçu pour refléter une importation de milliers de lignes sur beaucoup plus de colonnes, la différence pourrait être énorme.

JM4
la source
@ JM4 - excellente idée de mettre 10 lignes directement en une seule exécution . Mais comment insérer des milliers de lignes lorsqu'elles sont stockées dans un objet comme JSON? Mon code ci-dessous fonctionne parfaitement. Mais comment puis-je l'ajuster pour insérer 10 lignes en une seule exécution? `foreach ($ json_content as $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [date]; $ row3 = $ datarow [ligne3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [row6]; $ row7 = $ datarow [row7]; // exécute maintenant $ databaseinsert-> execute (); } // fin de foreach `
Peter
@ JM4 - ... et ma deuxième question est: "Pourquoi n'y a-t-il aucune bind_paramdéclaration dans la deuxième routine d'importation"?
Peter le
N'auriez-vous pas besoin de boucler deux fois? Vous auriez également à générer dynamiquement le (?,?), non?
NoobishPro
@NoobishPro Oui, vous pouvez utiliser le même for / foreach pour générer les deux.
Chazy Chaz
34

La réponse acceptée par Herbert Balagtas fonctionne bien lorsque le tableau $ data est petit. Avec des tableaux $ data plus grands, la fonction array_merge devient excessivement lente. Mon fichier de test pour créer le tableau $ data a 28 cols et fait environ 80 000 lignes. Le script final a pris 41 secondes pour se terminer.

L'utilisation de array_push () pour créer $ insert_values ​​au lieu de array_merge () a entraîné une accélération de 100X avec un temps d'exécution de 0,41 s .

Le problème array_merge ():

$insert_values = array();

foreach($data as $d){
 $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
 $insert_values = array_merge($insert_values, array_values($d));
}

Pour éliminer le besoin de array_merge (), vous pouvez créer les deux tableaux suivants à la place:

//Note that these fields are empty, but the field count should match the fields in $datafields.
$data[] = array('','','','',... n ); 

//getting rid of array_merge()
array_push($insert_values, $value1, $value2, $value3 ... n ); 

Ces tableaux peuvent ensuite être utilisés comme suit:

function placeholders($text, $count=0, $separator=","){
    $result = array();
    if($count > 0){
        for($x=0; $x<$count; $x++){
            $result[] = $text;
        }
    }

    return implode($separator, $result);
}

$pdo->beginTransaction();

foreach($data as $d){
 $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
}

$sql = "INSERT INTO table (" . implode(",", array_keys($datafield) ) . ") VALUES " . implode(',', $question_marks);

$stmt = $pdo->prepare ($sql);
try {
    $stmt->execute($insert_values);
} catch (PDOException $e){
    echo $e->getMessage();
}
$pdo->commit();
Chris M.
la source
4
En PHP 5.6, vous pouvez faire à la array_push($data, ...array_values($row))place de $data = array_merge($data, array_values($row));. Plus vite.
mpen
Pourquoi 5.6? La documentation ne dit rien sur la version 5.6, array_push()est disponible même en php 4.
ZurabWeb
1
@Piero c'est uniquement du code PHP 5.6+ non pas à cause de l'utilisation de array_push(), mais parce que @Mark utilise la décompression d'arguments. Remarquez l' ...array_values()appel là-bas?
mariano.iglesias
@ mariano.iglesias array_values()est également disponible en php 4. Je ne sais pas si c'est ce que vous entendez par là argument unpacking.
ZurabWeb
2
@Piero, le déballage des arguments est une fonctionnalité introduite dans PHP 5.6. C'est un moyen de fournir plusieurs arguments sous forme de tableau. Vérifiez ici - php.net/manual/en
Anis
14

Deux approches possibles:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:v1_1, :v1_2, :v1_3),
    (:v2_1, :v2_2, :v2_3),
    (:v2_1, :v2_2, :v2_3)');
$stmt->bindValue(':v1_1', $data[0][0]);
$stmt->bindValue(':v1_2', $data[0][1]);
$stmt->bindValue(':v1_3', $data[0][2]);
// etc...
$stmt->execute();

Ou:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:a, :b, :c)');
foreach($data as $item)
{
    $stmt->bindValue(':a', $item[0]);
    $stmt->bindValue(':b', $item[1]);
    $stmt->bindValue(':c', $item[2]);
    $stmt->execute();
}

Si les données de toutes les lignes sont dans un seul tableau, j'utiliserais la deuxième solution.

Zyx
la source
10
dans ce dernier cas, ne faites-vous pas alors plusieurs (éventuellement des milliers) d'appels d'exécution séparés au lieu de les combiner en une seule instruction?
JM4
@ JM4, suggérez-vous qu'il $stmt->execute();devrait être en dehors de la boucle foreach?
bafromca
@bafromca - Oui, je le suis. Voir ma réponse ci-dessus avec des votes positifs. Sur une instruction d'insertion pure, il n'y a aucune raison pour laquelle je peux logiquement penser qu'il ne peut pas s'agir d'une seule instruction. Un appel, une exécution. En fait, ma réponse du début de 2012 pourrait être encore améliorée - ce que je ferai plus tard quand j'aurai plus de temps. Si vous commencez à lancer des combinaisons d'insertion / mise à jour / suppression, c'est une autre histoire.
JM4
12

Ce n'est tout simplement pas la façon dont vous utilisez les déclarations préparées.

Il est parfaitement acceptable d'insérer une ligne par requête car vous pouvez exécuter une instruction préparée plusieurs fois avec des paramètres différents. En fait, c'est l'un des plus grands avantages car il vous permet d'insérer un grand nombre de rangées de manière efficace, sûre et confortable.

Il est donc peut-être possible de mettre en œuvre le schéma que vous proposez, au moins pour un nombre fixe de lignes, mais il est presque garanti que ce n'est pas vraiment ce que vous voulez.

sebasgo
la source
1
Pouvez-vous suggérer une meilleure façon d'insérer plusieurs lignes dans un tableau?
Crashthatch le
@Crashthatch: Faites-le simplement de manière naïve: configurez l'instruction préparée une fois, puis exécutez-la pour chaque ligne avec des valeurs différentes pour les paramètres liés. C'est la deuxième approche dans la réponse de Zyk.
sebasgo
2
Le but que vous avez mentionné pour la déclaration préparée est juste. Mais, l'utilisation de la multi-insertion est une autre technique pour améliorer la vitesse d'insertion et elle peut également être utilisée avec une instruction préparée. D'après mon expérience, lors de la migration de 30 millions de données de ligne à l'aide d'une déclaration préparée PDO, j'ai vu que l'insertion multiple était 7 à 10 fois plus rapide que l'insertion unique groupée dans les transactions.
Anis
1
Absolument d'accord avec Anis. J'ai 100000 lignes et j'obtiens une énorme augmentation de vitesse avec des inserts à plusieurs rangées.
Kenneth
Affirmer qu'appeler une base de données relationnelle en boucle une fois par ligne est généralement une bonne chose est une chose avec laquelle je ne suis pas d'accord. Votez contre cela. Certes, parfois ça va. Je ne crois pas aux absolus de l'ingénierie. Mais c'est un anti-pattern qui ne devrait être utilisé que dans certains cas.
Brandon
8

Une réponse plus courte: aplatissez le tableau de données triées par colonnes puis

//$array = array( '1','2','3','4','5', '1','2','3','4','5');
$arCount = count($array);
$rCount = ($arCount  ? $arCount - 1 : 0);
$criteria = sprintf("(?,?,?,?,?)%s", str_repeat(",(?,?,?,?,?)", $rCount));
$sql = "INSERT INTO table(c1,c2,c3,c4,c5) VALUES$criteria";

Lorsque vous insérez environ 1000 enregistrements, vous ne voulez pas avoir à parcourir chaque enregistrement pour les insérer lorsque tout ce dont vous avez besoin est un compte des valeurs.

fyrye
la source
5

Voici mon approche simple.

    $values = array();
    foreach($workouts_id as $value){
      $_value = "(".$value.",".$plan_id.")";
      array_push($values,$_value);
    }
    $values_ = implode(",",$values);

    $sql = "INSERT INTO plan_days(id,name) VALUES" . $values_."";
    $stmt = $this->conn->prepare($sql);
    $stmt->execute();

la source
6
vous battez le point d'utiliser des déclarations préparées. l'op est préoccupé par la sécurité dans la questionOn the readings on PDO, the use prepared statements should give me a better security than static queries.
YesItsMe
2
Juste une image que vous n'avez pas validée $workouts_id, ce qui peut avoir des $valuedonnées assez inattendues. Vous ne pouvez pas garantir que peut-être pas maintenant, mais à l'avenir, un autre développeur rendra ces données non sécurisées. Je pense donc que tout à fait plus juste faire la requête préparée par PDO.
Nikita_kharkov_ua
3

Voici une classe que j'ai écrite pour faire plusieurs insertions avec l'option de purge:

<?php

/**
 * $pdo->beginTransaction();
 * $pmi = new PDOMultiLineInserter($pdo, "foo", array("a","b","c","e"), 10);
 * $pmi->insertRow($data);
 * ....
 * $pmi->insertRow($data);
 * $pmi->purgeRemainingInserts();
 * $pdo->commit();
 *
 */
class PDOMultiLineInserter {
    private $_purgeAtCount;
    private $_bigInsertQuery, $_singleInsertQuery;
    private $_currentlyInsertingRows  = array();
    private $_currentlyInsertingCount = 0;
    private $_numberOfFields;
    private $_error;
    private $_insertCount = 0;

    function __construct(\PDO $pdo, $tableName, $fieldsAsArray, $bigInsertCount = 100) {
        $this->_numberOfFields = count($fieldsAsArray);
        $insertIntoPortion = "INSERT INTO `$tableName` (`".implode("`,`", $fieldsAsArray)."`) VALUES";
        $questionMarks  = " (?".str_repeat(",?", $this->_numberOfFields - 1).")";

        $this->_purgeAtCount = $bigInsertCount;
        $this->_bigInsertQuery    = $pdo->prepare($insertIntoPortion.$questionMarks.str_repeat(", ".$questionMarks, $bigInsertCount - 1));
        $this->_singleInsertQuery = $pdo->prepare($insertIntoPortion.$questionMarks);
    }

    function insertRow($rowData) {
        // @todo Compare speed
        // $this->_currentlyInsertingRows = array_merge($this->_currentlyInsertingRows, $rowData);
        foreach($rowData as $v) array_push($this->_currentlyInsertingRows, $v);
        //
        if (++$this->_currentlyInsertingCount == $this->_purgeAtCount) {
            if ($this->_bigInsertQuery->execute($this->_currentlyInsertingRows) === FALSE) {
                $this->_error = "Failed to perform a multi-insert (after {$this->_insertCount} inserts), the following errors occurred:".implode('<br/>', $this->_bigInsertQuery->errorInfo());
                return false;
            }
            $this->_insertCount++;

            $this->_currentlyInsertingCount = 0;
            $this->_currentlyInsertingRows = array();
        }
        return true;
    }

    function purgeRemainingInserts() {
        while ($this->_currentlyInsertingCount > 0) {
            $singleInsertData = array();
            // @todo Compare speed - http://www.evardsson.com/blog/2010/02/05/comparing-php-array_shift-to-array_pop/
            // for ($i = 0; $i < $this->_numberOfFields; $i++) $singleInsertData[] = array_pop($this->_currentlyInsertingRows); array_reverse($singleInsertData);
            for ($i = 0; $i < $this->_numberOfFields; $i++) array_unshift($singleInsertData, array_pop($this->_currentlyInsertingRows));

            if ($this->_singleInsertQuery->execute($singleInsertData) === FALSE) {
                $this->_error = "Failed to perform a small-insert (whilst purging the remaining rows; the following errors occurred:".implode('<br/>', $this->_singleInsertQuery->errorInfo());
                return false;
            }
            $this->_currentlyInsertingCount--;
        }
    }

    public function getError() {
        return $this->_error;
    }
}
Pierre Dumuid
la source
Bonjour Pierre. Peut-être que vous n'êtes plus actif ici. Néanmoins, je voulais juste souligner que mon idée pour ce numéro semble presque identique à la vôtre. Pure coïncidence, car je suppose qu'il n'y a pas grand-chose d'autre à cela. J'ai également ajouté des classes pour DELETE- AND UPDATE-Operations et j'ai impliqué quelques idées d'ici, par la suite. Je n'ai simplement pas vu votre classe. Veuillez excuser mon auto-promotion éhontée ici, mais je suppose que ce sera utile pour quelqu'un. J'espère que ce n'est pas contre les règles SO. Trouvez-le ici .
JackLeEmmerdeur
1

Voici comment je l'ai fait:

Définissez d'abord les noms de colonnes que vous utiliserez, ou laissez ce champ vide et pdo supposera que vous voulez utiliser toutes les colonnes de la table - auquel cas vous devrez informer les valeurs de ligne dans l'ordre exact où elles apparaissent sur la table .

$cols = 'name', 'middleName', 'eMail';
$table = 'people';

Maintenant, supposons que vous ayez déjà préparé un tableau à deux dimensions. Itérez-le et construisez une chaîne avec vos valeurs de ligne, comme suit:

foreach ( $people as $person ) {
if(! $rowVals ) {
$rows = '(' . "'$name'" . ',' . "'$middleName'" . ',' .           "'$eMail'" . ')';
} else { $rowVals  = '(' . "'$name'" . ',' . "'$middleName'" . ',' . "'$eMail'" . ')';
}

Maintenant, vous venez de vérifier si $ rows était déjà défini, et sinon, créez-le et stockez les valeurs de ligne et la syntaxe SQL nécessaire pour que ce soit une instruction valide. Notez que les chaînes doivent être placées entre guillemets doubles et simples, afin qu'elles soient rapidement reconnues comme telles.

Il ne reste plus qu'à préparer l'instruction et à l'exécuter, en tant que telle:

$stmt = $db->prepare ( "INSERT INTO $table $cols VALUES $rowVals" );
$stmt->execute ();

Testé avec jusqu'à 2000 lignes jusqu'à présent, et le temps d'exécution est lamentable. Fera quelques tests supplémentaires et reviendra ici au cas où j'aurais quelque chose de plus à apporter.

Cordialement.

Théo T. Carranza
la source
1

Comme cela n'a pas encore été suggéré, je suis à peu près sûr que LOAD DATA INFILE est toujours le moyen le plus rapide de charger des données car il désactive l'indexation, insère toutes les données, puis réactive les index - le tout en une seule demande.

L'enregistrement des données au format csv devrait être assez simple en gardant à l'esprit fputcsv. MyISAM est le plus rapide, mais vous obtenez toujours de grandes performances dans InnoDB. Il y a d'autres inconvénients, donc je choisirais cette voie si vous insérez beaucoup de données et que vous ne vous embêtez pas avec moins de 100 lignes.

avatarofhope2
la source
1

Bien qu'une vieille question m'ait beaucoup aidé, voici ma solution, qui fonctionne dans ma propre DbContextclasse. Le $rowsparamètre est simplement un tableau de tableaux associatifs représentant des lignes ou des modèles: field name => insert value.

Si vous utilisez un modèle qui utilise des modèles, cela s'intègre parfaitement lorsque les données du modèle sont transmises sous forme de tableau, par exemple à partir d'une ToRowArrayméthode de la classe de modèle.

Remarque : Cela devrait aller de soi, mais ne jamais permettre aux arguments passés à cette méthode d'être exposés à l'utilisateur ou dépendant de toute entrée utilisateur, autre que les valeurs d'insertion, qui ont été validées et nettoyées. L' $tableNameargument et les noms de colonne doivent être définis par la logique d'appel; par exemple, un Usermodèle peut être mappé à la table utilisateur, dont la liste de colonnes est mappée aux champs membres du modèle.

public function InsertRange($tableName, $rows)
{
    // Get column list
    $columnList = array_keys($rows[0]);
    $numColumns = count($columnList);
    $columnListString = implode(",", $columnList);

    // Generate pdo param placeholders
    $placeHolders = array();

    foreach($rows as $row)
    {
        $temp = array();

        for($i = 0; $i < count($row); $i++)
            $temp[] = "?";

        $placeHolders[] = "(" . implode(",", $temp) . ")";
    }

    $placeHolders = implode(",", $placeHolders);

    // Construct the query
    $sql = "insert into $tableName ($columnListString) values $placeHolders";
    $stmt = $this->pdo->prepare($sql);

    $j = 1;
    foreach($rows as $row)
    {
        for($i = 0; $i < $numColumns; $i++)
        {
            $stmt->bindParam($j, $row[$columnList[$i]]);
            $j++;
        }
    }

    $stmt->execute();
}
Lee
la source
se débarrasser d'une transaction, car cela n'a aucun sens d'en utiliser une pour une seule requête. et comme d'habitude, ce code est vulnérable à une injection SQL ou à une erreur de requête.
Your Common Sense
Vous avez raison sur l'utilisation redondante des transactions pour ce cas, mais je ne vois pas en quoi cela est vulnérable à l'injection SQL. Il est paramétré donc je ne peux que supposer que vous supposez qu'il $tableNameest exposé à l'utilisateur, ce qui n'est pas le cas, c'est dans la DAL. Pouvez-vous développer vos revendications? Il n'est pas utile de simplement dire des choses.
Lee
Eh bien, ce n'est pas seulement un nom de table mais de toute façon: comment savoir s'il sera exposé ou non par quelqu'un qui utiliserait le code que vous avez posté ici?
Votre bon sens
Il est donc de la responsabilité d'un poster de décrire chaque utilisation potentielle du code ou chaque source d'arguments? Peut-être ai-je des attentes plus élevées envers les gens. Cela vous rendrait-il plus heureux si j'ajoutais une note pour ne pas autoriser l'utilisateur à avoir accès $tableName?
Lee
C'est la responsabilité de l'affiche de publier un code fiable, si son intention est d'aider quelqu'un, pas seulement de se montrer.
Votre bon sens
1

Voici une autre solution (mince) pour ce problème:

Au début, vous devez compter les données du tableau source (ici: $ aData) avec count (). Ensuite, vous utilisez array_fill () et générez un nouveau tableau avec autant d'entrées que le tableau source a, chacune avec la valeur "(?,?)" (Le nombre d'espaces réservés dépend des champs que vous utilisez; ici: 2). Ensuite, le tableau généré doit être implosé et comme colle une virgule est utilisée. Dans la boucle foreach, vous devez générer un autre index concernant le nombre d'espaces réservés que vous utilisez (nombre d'espaces réservés * index du tableau actuel + 1). Vous devez ajouter 1 à l'index généré après chaque valeur liée.

$do = $db->prepare("INSERT INTO table (id, name) VALUES ".implode(',', array_fill(0, count($aData), '(?,?)')));

foreach($aData as $iIndex => $aValues){
 $iRealIndex = 2 * $iIndex + 1;
 $do->bindValue($iRealIndex, $aValues['id'], PDO::PARAM_INT);
 $iRealIndex = $iRealIndex + 1;
 $do->bindValue($iRealIndex, $aValues['name'], PDO::PARAM_STR);
}

$do->execute();
Bernhard
la source
0

Vous pouvez insérer plusieurs lignes dans une seule requête avec cette fonction:

function insertMultiple($query,$rows) {
    if (count($rows)>0) {
        $args = array_fill(0, count($rows[0]), '?');

        $params = array();
        foreach($rows as $row)
        {
            $values[] = "(".implode(',', $args).")";
            foreach($row as $value)
            {
                $params[] = $value;
            }
        }

        $query = $query." VALUES ".implode(',', $values);
        $stmt = $PDO->prepare($query);
        $stmt->execute($params);
    }
}

$ row est un tableau de tableaux de valeurs. Dans votre cas, vous appelleriez la fonction avec

insertMultiple("INSERT INTO tbl (`key1`,`key2`)",array(array('r1v1','r1v2'),array('r2v1','r2v2')));

Cela présente l'avantage d'utiliser des instructions préparées , tout en insérant plusieurs lignes avec une seule requête. Sécurité!

Chris Michaelides
la source
0

Voici ma solution: https://github.com/sasha-ch/Aura.Sql basée sur la bibliothèque auraphp / Aura.Sql.

Exemple d'utilisation:

$q = "insert into t2(id,name) values (?,?), ... on duplicate key update name=name"; 
$bind_values = [ [[1,'str1'],[2,'str2']] ];
$pdo->perform($q, $bind_values);

Les rapports de bogues sont les bienvenus.

sasha-ch
la source
À partir de 2.4, vous pouvez créer des insertions multiples avec github.com/auraphp/Aura.SqlQuery/tree/… et utiliser ExtendedPdo pour exécuter :).
Hari KT
0

Mon exemple du monde réel pour insérer tous les codes postaux allemands dans un tableau vide (pour ajouter des noms de villes plus tard):

// obtain column template
$stmt = $db->prepare('SHOW COLUMNS FROM towns');
$stmt->execute();
$columns = array_fill_keys(array_values($stmt->fetchAll(PDO::FETCH_COLUMN)), null);
// multiple INSERT
$postcode = '01000';// smallest german postcode
while ($postcode <= 99999) {// highest german postcode
    $values = array();
    while ($postcode <= 99999) {
        // reset row
        $row = $columns;
        // now fill our row with data
        $row['postcode'] = sprintf('%05d', $postcode);
        // build INSERT array
        foreach ($row as $value) {
            $values[] = $value;
        }
        $postcode++;
        // avoid memory kill
        if (!($postcode % 10000)) {
            break;
        }
    }
    // build query
    $count_columns = count($columns);
    $placeholder = ',(' . substr(str_repeat(',?', $count_columns), 1) . ')';//,(?,?,?)
    $placeholder_group = substr(str_repeat($placeholder, count($values) / $count_columns), 1);//(?,?,?),(?,?,?)...
    $into_columns = implode(',', array_keys($columns));//col1,col2,col3
    // this part is optional:
    $on_duplicate = array();
    foreach ($columns as $column => $row) {
        $on_duplicate[] = $column;
        $on_duplicate[] = $column;
    }
    $on_duplicate = ' ON DUPLICATE KEY UPDATE' . vsprintf(substr(str_repeat(', %s = VALUES(%s)', $count_columns), 1), $on_duplicate);
    // execute query
    $stmt = $db->prepare('INSERT INTO towns (' . $into_columns . ') VALUES' . $placeholder_group . $on_duplicate);//INSERT INTO towns (col1,col2,col3) VALUES(?,?,?),(?,?,?)... {ON DUPLICATE...}
    $stmt->execute($values);
}

Comme vous pouvez le voir, il est entièrement flexible. Vous n'avez pas besoin de vérifier le nombre de colonnes ou de vérifier la position de votre colonne. Il vous suffit de définir les données d'insertion:

    $row['postcode'] = sprintf('%05d', $postcode);

Je suis fier de certains des constructeurs de chaînes de requête car ils fonctionnent sans fonctions de tableau lourdes comme array_merge. Surtout vsprintf () était une bonne trouvaille.

Enfin, j'ai dû ajouter 2x while () pour éviter de dépasser la limite de mémoire. Cela dépend de votre limite de mémoire, mais c'est une bonne solution générale pour éviter les problèmes (et avoir 10 requêtes est toujours bien mieux que 10.000).

mgutt
la source
0

test.php

<?php
require_once('Database.php');

$obj = new Database();
$table = "test";

$rows = array(
    array(
    'name' => 'balasubramani',
    'status' => 1
    ),
    array(
    'name' => 'balakumar',
    'status' => 1
    ),
    array(
    'name' => 'mani',
    'status' => 1
    )
);

var_dump($obj->insertMultiple($table,$rows));
?>

Database.php

<?php
class Database 
{

    /* Initializing Database Information */

    var $host = 'localhost';
    var $user = 'root';
    var $pass = '';
    var $database = "database";
    var $dbh;

    /* Connecting Datbase */

    public function __construct(){
        try {
            $this->dbh = new PDO('mysql:host='.$this->host.';dbname='.$this->database.'', $this->user, $this->pass);
            //print "Connected Successfully";
        } 
        catch (PDOException $e) {
            print "Error!: " . $e->getMessage() . "<br/>";
            die();
        }
    }
/* Insert Multiple Rows in a table */

    public function insertMultiple($table,$rows){

        $this->dbh->beginTransaction(); // also helps speed up your inserts.
        $insert_values = array();
        foreach($rows as $d){
            $question_marks[] = '('  . $this->placeholders('?', sizeof($d)) . ')';
            $insert_values = array_merge($insert_values, array_values($d));
            $datafields = array_keys($d);
        }

        $sql = "INSERT INTO $table (" . implode(",", $datafields ) . ") VALUES " . implode(',', $question_marks);

        $stmt = $this->dbh->prepare ($sql);
        try {
            $stmt->execute($insert_values);
        } catch (PDOException $e){
            echo $e->getMessage();
        }
        return $this->dbh->commit();
    }

    /*  placeholders for prepared statements like (?,?,?)  */

    function placeholders($text, $count=0, $separator=","){
        $result = array();
        if($count > 0){
            for($x=0; $x<$count; $x++){
                $result[] = $text;
            }
        }

        return implode($separator, $result);
    }

}
?>
sonofkrish
la source
Bienvenue dans stackoverflow. Pas seulement le code, veuillez poster votre problème et expliquer.
Prakash Palnati
fondamentalement. c'est juste une implémentation du code fourni dans la réponse acceptée
Votre bon sens
0

J'ai eu le même problème et c'est ainsi que j'accomplis pour moi-même, et je me suis créé une fonction pour cela (et vous pouvez l'utiliser si cela vous aide).

Exemple:

INSÉRER DANS LES VALEURS des pays (pays, ville) (Allemagne, Berlin), (France, Paris);

$arr1 = Array("Germany", "Berlin");
$arr2 = Array("France", "France");

insertMultipleData("countries", Array($arr1, $arr2));


// Inserting multiple data to the Database.
public function insertMultipleData($table, $multi_params){
    try{
        $db = $this->connect();

        $beforeParams = "";
        $paramsStr = "";
        $valuesStr = "";

        for ($i=0; $i < count($multi_params); $i++) { 

            foreach ($multi_params[$i] as $j => $value) {                   

                if ($i == 0) {
                    $beforeParams .=  " " . $j . ",";
                }

                $paramsStr .= " :"  . $j . "_" . $i .",";                                       
            }

            $paramsStr = substr_replace($paramsStr, "", -1);
            $valuesStr .=  "(" . $paramsStr . "),"; 
            $paramsStr = "";
        }


        $beforeParams = substr_replace($beforeParams, "", -1);
        $valuesStr = substr_replace($valuesStr, "", -1);


        $sql = "INSERT INTO " . $table . " (" . $beforeParams . ") VALUES " . $valuesStr . ";";

        $stmt = $db->prepare($sql);


        for ($i=0; $i < count($multi_params); $i++) { 
            foreach ($multi_params[$i] as $j => &$value) {
                $stmt->bindParam(":" . $j . "_" . $i, $value);                                      
            }
        }

        $this->close($db);
        $stmt->execute();                       

        return true;

    }catch(PDOException $e){            
        return false;
    }

    return false;
}

// Making connection to the Database 
    public function connect(){
        $host = Constants::DB_HOST;
        $dbname = Constants::DB_NAME;
        $user = Constants::DB_USER;
        $pass = Constants::DB_PASS;

        $mysql_connect_str = 'mysql:host='. $host . ';dbname=' .$dbname;

        $dbConnection = new PDO($mysql_connect_str, $user, $pass);
        $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        return $dbConnection;
    }

    // Closing the connection
    public function close($db){
        $db = null;
    }

Si insertMultipleData ($ table, $ multi_params) renvoie TRUE , vos données ont été insérées dans votre base de données.

Dardan
la source
0

Sur la base de mes expériences, j'ai découvert que l'instruction d'insertion mysql avec plusieurs lignes de valeur dans une seule transaction est la plus rapide.

Cependant, si les données sont trop nombreuses, le max_allowed_packetparamètre de mysql peut restreindre l'insertion de transaction unique avec plusieurs lignes de valeurs. Par conséquent, les fonctions suivantes échoueront lorsqu'il y a des données supérieures à la max_allowed_packettaille de mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

La transactionSpeedméthode la plus réussie dans le scénario d'insertion de données volumineuses est la méthode, mais elle prend plus de temps que les méthodes mentionnées ci-dessus. Ainsi, pour gérer ce problème, vous pouvez diviser vos données en plus petits morceaux et appeler plusieurs fois l'insertion de transaction unique ou abandonner la vitesse d'exécution en utilisant la transactionSpeedméthode.

Voici mes recherches

<?php

class SpeedTestClass
{
    private $data;

    private $pdo;

    public function __construct()
    {
        $this->data = [];
        $this->pdo = new \PDO('mysql:dbname=test_data', 'admin', 'admin');
        if (!$this->pdo) {
            die('Failed to connect to database');
        }
    }

    public function createData()
    {
        $prefix = 'test';
        $postfix = 'unicourt.com';
        $salutations = ['Mr.', 'Ms.', 'Dr.', 'Mrs.'];

        $csv[] = ['Salutation', 'First Name', 'Last Name', 'Email Address'];
        for ($i = 0; $i < 100000; ++$i) {
            $csv[] = [
                $salutations[$i % \count($salutations)],
                $prefix.$i,
                $prefix.$i,
                $prefix.$i.'@'.$postfix,
            ];
        }

        $this->data = $csv;
    }

    public function truncateTable()
    {
        $this->pdo->query('TRUNCATE TABLE `name`');
    }

    public function transactionSpeed()
    {
        $timer1 = microtime(true);
        $this->pdo->beginTransaction();
        $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES (:first_name, :last_name)';
        $sth = $this->pdo->prepare($sql);

        foreach (\array_slice($this->data, 1) as $values) {
            $sth->execute([
                ':first_name' => $values[1],
                ':last_name' => $values[2],
            ]);
        }

        // $timer2 = microtime(true);
        // echo 'Prepare Time: '.($timer2 - $timer1).PHP_EOL;
        // $timer3 = microtime(true);

        if (!$this->pdo->commit()) {
            echo "Commit failed\n";
        }
        $timer4 = microtime(true);
        // echo 'Commit Time: '.($timer4 - $timer3).PHP_EOL;

        return $timer4 - $timer1;
    }

    public function autoCommitSpeed()
    {
        $timer1 = microtime(true);
        $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES (:first_name, :last_name)';
        $sth = $this->pdo->prepare($sql);
        foreach (\array_slice($this->data, 1) as $values) {
            $sth->execute([
                ':first_name' => $values[1],
                ':last_name' => $values[2],
            ]);
        }
        $timer2 = microtime(true);

        return $timer2 - $timer1;
    }

    public function noBindAutoCommitSpeed()
    {
        $timer1 = microtime(true);

        foreach (\array_slice($this->data, 1) as $values) {
            $sth = $this->pdo->prepare("INSERT INTO `name` (`first_name`, `last_name`) VALUES ('{$values[1]}', '{$values[2]}')");
            $sth->execute();
        }
        $timer2 = microtime(true);

        return $timer2 - $timer1;
    }

    public function singleTransactionInsert()
    {
        $timer1 = microtime(true);
        foreach (\array_slice($this->data, 1) as $values) {
            $arr[] = "('{$values[1]}', '{$values[2]}')";
        }
        $sth = $this->pdo->prepare('INSERT INTO `name` (`first_name`, `last_name`) VALUES '.implode(', ', $arr));
        $sth->execute();
        $timer2 = microtime(true);

        return $timer2 - $timer1;
    }

    public function singleTransactionInsertWithPlaceholders()
    {
        $placeholders = [];
        $timer1 = microtime(true);
        $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES ';
        foreach (\array_slice($this->data, 1) as $values) {
            $placeholders[] = '(?, ?)';
            $arr[] = $values[1];
            $arr[] = $values[2];
        }
        $sql .= implode(', ', $placeholders);
        $sth = $this->pdo->prepare($sql);
        $sth->execute($arr);
        $timer2 = microtime(true);

        return $timer2 - $timer1;
    }

    public function singleTransactionInsertWithRollback()
    {
        $placeholders = [];
        $timer1 = microtime(true);
        $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES ';
        foreach (\array_slice($this->data, 1) as $values) {
            $placeholders[] = '(?, ?)';
            $arr[] = $values[1];
            $arr[] = $values[2];
        }
        $sql .= implode(', ', $placeholders);
        $this->pdo->beginTransaction();
        $sth = $this->pdo->prepare($sql);
        $sth->execute($arr);
        $this->pdo->commit();
        $timer2 = microtime(true);

        return $timer2 - $timer1;
    }
}

$s = new SpeedTestClass();
$s->createData();
$s->truncateTable();
echo "Time Spent for singleTransactionInsertWithRollback: {$s->singleTransactionInsertWithRollback()}".PHP_EOL;
$s->truncateTable();
echo "Time Spent for single Transaction Insert: {$s->singleTransactionInsert()}".PHP_EOL;
$s->truncateTable();
echo "Time Spent for single Transaction Insert With Placeholders: {$s->singleTransactionInsertWithPlaceholders()}".PHP_EOL;
$s->truncateTable();
echo "Time Spent for transaction: {$s->transactionSpeed()}".PHP_EOL;
$s->truncateTable();
echo "Time Spent for AutoCommit: {$s->noBindAutoCommitSpeed()}".PHP_EOL;
$s->truncateTable();
echo "Time Spent for autocommit with bind: {$s->autoCommitSpeed()}".PHP_EOL;
$s->truncateTable();

Les résultats pour 100 000 entrées pour un tableau contenant seulement deux colonnes sont les suivants

$ php data.php
Time Spent for singleTransactionInsertWithRollback: 0.75147604942322
Time Spent for single Transaction Insert: 0.67445182800293
Time Spent for single Transaction Insert With Placeholders: 0.71131205558777
Time Spent for transaction: 8.0056409835815
Time Spent for AutoCommit: 35.4979159832
Time Spent for autocommit with bind: 33.303519010544
theBuzzyCoder
la source
0

Cela a fonctionné pour moi

$sql = 'INSERT INTO table(pk_pk1,pk_pk2,date,pk_3) VALUES '; 
$qPart = array_fill(0, count($array), "(?, ?,UTC_TIMESTAMP(),?)");
$sql .= implode(",", $qPart);
$stmt =    DB::prepare('base', $sql);
$i = 1;
foreach ($array as $value) { 
  $stmt->bindValue($i++, $value);
  $stmt->bindValue($i++, $pk_pk1);
  $stmt->bindValue($i++, $pk_pk2); 
  $stmt->bindValue($i++, $pk_pk3); 
} 
$stmt->execute();
Andre Da Silva Poppi
la source
0

qu'en est-il de quelque chose comme ça:

        if(count($types_of_values)>0){
         $uid = 1;
         $x = 0;
         $sql = "";
         $values = array();
          foreach($types_of_values as $k=>$v){
            $sql .= "(:id_$k,:kind_of_val_$k), ";
            $values[":id_$k"] = $uid;
            $values[":kind_of_val_$k"] = $v;
          }
         $sql = substr($sql,0,-2);
         $query = "INSERT INTO table (id,value_type) VALUES $sql";
         $res = $this->db->prepare($query);
         $res->execute($values);            
        }

L'idée sous-jacente est de parcourir les valeurs de votre tableau, en ajoutant des «numéros d'identification» à chaque boucle pour vos espaces réservés d'instructions préparées, tandis qu'en même temps, vous ajoutez les valeurs à votre tableau pour les paramètres de liaison. Si vous n'aimez pas utiliser l'index "key" du tableau, vous pouvez ajouter $ i = 0 et $ i ++ dans la boucle. Les deux fonctionnent dans cet exemple, même si vous avez des tableaux associatifs avec des clés nommées, cela fonctionnerait toujours à condition que les clés soient uniques. Avec un peu de travail, ce serait aussi bien pour les tableaux imbriqués.

** Notez que substr supprime le dernier espace et virgule des variables $ sql, si vous n'avez pas d'espace, vous devez le changer en -1 plutôt que -2.

dean williams
la source
-1

La plupart des solutions données ici pour créer la requête préparée sont plus complexes qu'elles doivent l'être. En utilisant les fonctions intégrées de PHP, vous pouvez facilement créer l'instruction SQL sans frais généraux importants.

Étant donné $records, un tableau d'enregistrements où chaque enregistrement est lui-même un tableau indexé (sous la forme de field => value), la fonction suivante insérera les enregistrements dans la table donnée $table, sur une connexion PDO $connection, en n'utilisant qu'une seule instruction préparée. Notez qu'il s'agit d'une solution PHP 5.6+ en raison de l'utilisation de la décompression d'arguments dans l'appel à array_push:

private function import(PDO $connection, $table, array $records)
{
    $fields = array_keys($records[0]);
    $placeHolders = substr(str_repeat(',?', count($fields)), 1);
    $values = [];
    foreach ($records as $record) {
        array_push($values, ...array_values($record));
    }

    $query = 'INSERT INTO ' . $table . ' (';
    $query .= implode(',', $fields);
    $query .= ') VALUES (';
    $query .= implode('),(', array_fill(0, count($records), $placeHolders));
    $query .= ')';

    $statement = $connection->prepare($query);
    $statement->execute($values);
}
mariano.iglesias
la source
1
Ce code ne doit jamais être utilisé car il est vulnérable à l'injection SQL
Votre bon sens