Preparato DOP Inserisce più righe in una singola query


146

Attualmente sto usando questo tipo di SQL su MySQL per inserire più righe di valori in una singola query:

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

Sulle letture su DOP, l'uso delle istruzioni preparate dovrebbe darmi una sicurezza migliore rispetto alle query statiche.

Vorrei quindi sapere se è possibile generare "l'inserimento di più righe di valori mediante l'uso di una query" utilizzando istruzioni preparate.

Se sì, posso sapere come posso implementarlo?


attenzione con molte risposte per $stmt->execute($data); php.net/manual/it/… Fondamentalmente tutti i parametri sono passati validati come stringhe. Basta scorrere i dati dopo aver creato la query e aver passato manualmente bindValueo bindParampassato il tipo come terzo argomento.
MrMesees,

Risposte:


151

Inserimento di più valori con istruzioni preparate DOP

Inserimento di più valori in un'istruzione execute. Perché perché secondo questa pagina è più veloce degli inserti regolari.

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

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

più valori di dati o probabilmente hai un ciclo che popola i dati.

Con inserti preparati devi conoscere i campi in cui stai inserendo e il numero di campi per creare il? segnaposto per associare i tuoi parametri.

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

Questo è fondamentalmente il modo in cui vogliamo che assomigli all'istruzione insert.

Ora, il codice:

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

Sebbene nel mio test, ci fosse solo una differenza di 1 secondo quando si utilizzano più inserti e inserti preparati regolari con valore singolo.


4
Un errore di battitura, nella spiegazione sopra menziona $ datafields sebbene $ datafield sia usato in $ sql. Pertanto, copia incolla comporterebbe un errore. Per favore, rettifica. Grazie per questa soluzione però.
pal4life,

1
Usato questo per un po 'di tempo, poi ho notato che i valori con virgolette singole non sono salvati correttamente. L'uso delle doppie virgolette sull'implosione funziona come un incantesimo per me: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman,

1
array_merge sembra più costoso del semplice utilizzo di array_push.
K2xL,

14
Quando dici "c'era solo una differenza di 1 secondo", quante righe hai inserito un dato? 1 secondo è piuttosto significativo a seconda del contesto.
Kevin Dice,

3
Ottimizzazione: inutile chiamare placeholders()più e più volte. Chiamalo una volta prima del ciclo con sizeof($datafields)e aggiungi la stringa del risultato $question_marks[]all'interno del ciclo.
AVIDeveloper,

71

Stessa risposta del signor Balagtas, leggermente più chiara ...

Le versioni più recenti di MySQL e PHP DOP fanno di supporto multi-filaINSERT dichiarazioni.

Panoramica di SQL

L'SQL sarà simile a questo, assumendo una tabella a 3 colonne che vorresti INSERT.

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

ON DUPLICATE KEY UPDATEfunziona come previsto anche con un INSERT multi-riga; aggiungi questo:

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

Panoramica di PHP

Il vostro codice PHP seguirà le solite $pdo->prepare($qry)e $stmt->execute($params)chiamate DOP.

$paramssarà una matrice monodimensionale di tutti i valori da passare a INSERT.

Nell'esempio sopra, dovrebbe contenere 9 elementi; DOP utilizzerà ogni set di 3 come una singola riga di valori. (Inserimento di 3 righe di 3 colonne ciascuna = matrice di 9 elementi.)

Implementazione

Di seguito il codice è scritto per chiarezza, non per efficienza. Lavora con le array_*()funzioni PHP per modi migliori di mappare o esaminare i tuoi dati, se lo desideri. L'eventuale utilizzo delle transazioni dipende ovviamente dal tipo di tabella MySQL.

assumendo:

  • $tblName - il nome della stringa della tabella in cui INSERIRE
  • $colNames- Matrice monodimensionale dei nomi di colonna della tabella Questi nomi di colonna devono essere identificatori di colonna MySQL validi; sfuggirli con backtick (``) se non lo sono
  • $dataVals - array multidimensionale, in cui ogni elemento è un array 1-d di una riga di valori da INSERIRE

Codice di esempio

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

6
È davvero un peccato che il PDO lo gestisca in questo modo, ci sono alcuni modi molto eleganti per farlo in altri driver DB.
Jonathon

Questo prepara i segnaposto in modo ancora più conciso, rendendo $rowPlacesnon più necessario:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil

Funziona perfettamente Aggiungerei a questa risposta la necessità di garantire l'unicità degli indici (combinazione di) nella tabella. Come in ALTER TABLE votesADD UNIQUE unique_index( user, email, address);
Giuseppe,

1
Eccezionale! A proposito, l'uso array_push($dataToInsert, ...array_values($dataVals));sarà molto più veloce di alloraforeach ($dataVals as $row => $data) {}
Anis,

39

Per quello che vale, ho visto molti utenti raccomandare di ripetere le istruzioni INSERT invece di creare una query a stringa singola come ha fatto la risposta selezionata. Ho deciso di eseguire un semplice test con solo due campi e un'istruzione insert molto semplice:

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

Mentre la query complessiva stessa impiegava millisecondi o meno, quest'ultima (singola stringa) era costantemente 8 volte più veloce o più. Se questo è stato creato per dire che riflette un'importazione di migliaia di righe su molte più colonne, la differenza potrebbe essere enorme.


@ JM4 - ottima idea di mettere 10 file direttamente in un'unica esecuzione . Ma come posso inserire migliaia di righe quando sono archiviate in un oggetto come JSON? Il mio codice qui sotto funziona perfettamente. Ma come posso regolarlo per inserire 10 righe in un'unica esecuzione? `foreach ($ json_content as $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [data]; $ row3 = $ datarow [row3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [row6]; $ row7 = $ datarow [row7]; // ora esegue $ databaseinsert-> execute (); } // end of foreach `
Peter,

@ JM4 - ... e la mia seconda domanda è: "perché non ci sono bind_paramdichiarazioni nella seconda routine di importazione"?
Peter,

Non dovresti ripetere il ciclo due volte? Dovresti anche generare dinamicamente il (?,?), giusto?
NoobishPro

@NoobishPro Sì, puoi usare lo stesso per / foreach per generare entrambi.
Chazy Chaz,

34

La risposta accettata da Herbert Balagtas funziona bene quando l'array $ data è piccolo. Con array di dati $ più grandi, la funzione array_merge diventa proibitivamente lenta. Il mio file di test per creare l'array $ data ha 28 colonne ed è di circa 80.000 righe. Il completamento dello script finale ha richiesto 41 secondi .

L'uso di array_push () per creare $ insert_values ​​anziché array_merge () ha comportato una velocità di 100X con un tempo di esecuzione di 0,41 secondi .

La problematica array_merge ():

$insert_values = array();

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

Per eliminare la necessità di array_merge (), è possibile invece creare i due array seguenti:

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

Questi array possono quindi essere utilizzati come segue:

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

4
In PHP 5.6 puoi fare array_push($data, ...array_values($row))invece di $data = array_merge($data, array_values($row));. Più veloce.
aprono il

Perché 5.6? La documentazione non dice nulla su 5.6, array_push()è disponibile anche in php 4.
ZurabWeb

1
@Piero è solo il codice PHP 5.6+ non a causa dell'uso di array_push(), ma perché @Mark sta usando l'argomento decompressione. Notare la ...array_values()chiamata lì?
mariano.iglesias,

@ mariano.iglesias array_values()è anche disponibile in php 4. Non sono sicuro se questo è ciò che intendi argument unpacking.
ZurabWeb,

2
@Piero, il disimballaggio degli argomenti è una funzionalità introdotta in PHP 5.6. È un modo per fornire più argomenti come un array. Controlla qui - php.net/manual/it/…
Anis

14

Due possibili approcci:

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

O:

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

Se i dati per tutte le righe sono in un singolo array, utilizzerei la seconda soluzione.


10
in quest'ultimo caso non stai effettuando diverse (forse migliaia) di chiamate di esecuzione separate invece di combinarle in un'unica istruzione?
JM4,

@ JM4, stai suggerendo di $stmt->execute();essere al di fuori del ciclo foreach?
bafromca,

@bafromca - Sì, lo sono. Vedi la mia risposta sopra con voti positivi. Su una pura istruzione insert non c'è motivo per cui io possa logicamente pensare che non possa essere una singola istruzione. Una chiamata, una eseguita. In effetti, la mia risposta dall'inizio del 2012 potrebbe essere ulteriormente migliorata, cosa che farò in seguito quando avrò ancora un po 'di tempo. Se inizi a lanciare combinazioni Inserisci / Aggiorna / Elimina, questa è una storia diversa.
JM4

12

Semplicemente non è così che usi dichiarazioni preparate.

È perfettamente corretto inserire una riga per query poiché è possibile eseguire più volte un'istruzione preparata con parametri diversi. In effetti, questo è uno dei maggiori vantaggi in quanto consente di inserire un gran numero di file in modo efficiente, sicuro e confortevole.

Quindi forse è possibile implementare lo schema che stai proponendo, almeno per un numero fisso di righe, ma è quasi garantito che questo non è proprio quello che vuoi.


1
Puoi suggerire un modo migliore per inserire più righe in una tabella?
Crashthatch,

@ Crashthatch: fallo nel modo ingenuo: imposta una volta l'istruzione preparata, quindi eseguila per ogni riga con valori diversi per i parametri associati. Questo è il secondo approccio nella risposta di Zyk.
sebasgo

2
Lo scopo che hai citato per una dichiarazione preparata è giusto. Tuttavia, l'utilizzo di multiinserimento è un'altra tecnica per migliorare la velocità di inserimento e può essere utilizzata anche con istruzioni preparate. Nella mia esperienza, durante la migrazione di 30 milioni di dati di riga utilizzando la dichiarazione preparata DOP, ho visto l'inserimento multiplo 7-10 volte più veloce rispetto all'inserimento singolo dell'insieme nelle transazioni.
Anis,

1
Assolutamente d'accordo con Anis. Ho 100.000 file e ottengo un enorme aumento di velocità con inserti multi-fila.
Kenneth,

Affermare che chiamare un database relazionale in un ciclo una volta per riga è generalmente una buona cosa non posso essere d'accordo. Un voto negativo per quello. Concesso, a volte va bene. Non credo agli assoluti con l'ingegneria. Ma questo è un anti-pattern che dovrebbe essere usato solo in casi selezionati.
Brandon,

8

Una risposta più breve: appiattire quindi l'array di dati ordinati per colonne

//$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";

Quando si inseriscono circa 1.000 record, non è necessario scorrere in sequenza ogni record per inserirli quando tutto ciò che serve è un conteggio dei valori.


5

Ecco il mio approccio semplice.

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

6
stai sconfiggendo il punto di usare dichiarazioni preparate. L'op è preoccupato per la sicurezza nella domandaOn the readings on PDO, the use prepared statements should give me a better security than static queries.
Sì, il

2
Solo imaging che non hai convalidato $workouts_id, che può avere $values con dati abbastanza inaspettati. Non puoi garantire che forse non ora, ma in futuro un altro sviluppatore renderà questi dati insicuri. Quindi penso che sia più giusto fare la query preparata dal DOP.
Nikita_kharkov_ua

3

Ecco una classe che ho scritto per fare più inserti con l'opzione di eliminazione:

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

Ciao Pierre Forse non sei più attivo da queste parti. Tuttavia, volevo solo sottolineare che la mia idea per questo problema sembra quasi identica alla tua. Pura coincidenza, poiché immagino non ci sia molto altro. Ho aggiunto anche lezioni per le operazioni DELETE E UPDATE e ho seguito alcune idee da qui in poi. Non ho visto la tua lezione. Per favore, scusa la mia spudorata auto promozione qui, ma immagino che sarà di aiuto per qualcuno. Spero che questo non sia contrario alle SO-Rules. Lo trovi qui .
JackLeEmmerdeur il

1

Ecco come l'ho fatto:

Definisci prima i nomi delle colonne che utilizzerai o lascialo vuoto e pdo supporrà che desideri utilizzare tutte le colonne sulla tabella, nel qual caso dovrai informare i valori delle righe nell'ordine esatto in cui appaiono sulla tabella .

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

Supponiamo che tu abbia già preparato un array bidimensionale. Iterate e costruite una stringa con i vostri valori di riga, come tale:

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

Ora, quello che hai appena fatto è stato verificare se $ row era già stato definito e, in caso contrario, crearlo e archiviare i valori di riga e la sintassi SQL necessaria, in modo che sia un'istruzione valida. Si noti che le stringhe devono essere racchiuse tra virgolette doppie e virgolette singole, quindi verranno prontamente riconosciute come tali.

Non resta che preparare la dichiarazione ed eseguire, come tale:

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

Finora testato fino a 2000 file e il tempo di esecuzione è triste. Eseguirà altri test e tornerà qui nel caso avessi qualcosa in più da contribuire.

Saluti.


1

Dal momento che non è stato ancora suggerito, sono abbastanza sicuro che LOAD DATA INFILE sia ancora il modo più veloce per caricare i dati poiché disabilita l'indicizzazione, inserisce tutti i dati e quindi riattiva gli indici, il tutto in una singola richiesta.

Salvare i dati come CSV dovrebbe essere abbastanza banale tenendo presente fputcsv. MyISAM è più veloce, ma ottieni comunque grandi prestazioni in InnoDB. Ci sono altri svantaggi, anche se quindi inserirò molti dati e non mi preoccuperei di meno di 100 righe.


1

Anche se una vecchia domanda, tutti i contributi mi hanno aiutato molto, quindi ecco la mia soluzione, che funziona all'interno della mia DbContextclasse. Il $rowsparametro è semplicemente un array di array associativi rappresentano righe o modelli: field name => insert value.

Se si utilizza un modello che utilizza modelli che si adattano perfettamente quando si passano i dati del modello come matrice, ad esempio da un ToRowArraymetodo all'interno della classe del modello.

Nota : Va da sé, ma non consentire mai agli argomenti passati a questo metodo di essere esposti all'utente o di fare affidamento su qualsiasi input dell'utente, diverso dai valori di inserimento, che sono stati convalidati e disinfettati. L' $tableNameargomento e i nomi delle colonne dovrebbero essere definiti dalla logica chiamante; per esempio un Usermodello potrebbe essere mappato alla tabella utente, che ha il suo elenco di colonne mappato ai campi membri del modello.

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

sbarazzarsi di una transazione, poiché non ha senso usarne una per una singola query. e come al solito, questo codice è vulnerabile all'iniezione SQL o all'errore di query.
Il tuo buon senso

Hai ragione sull'uso ridondante delle transazioni per questo caso, ma non vedo come questo sia vulnerabile all'iniezione SQL. È parametrizzato, quindi posso solo presumere che stai assumendo che $tableNameè esposto all'utente, che non lo è, è nel DAL. Puoi espandere le tue richieste? Non è utile dire solo cose.
Lee,

beh, non è solo un nome di tabella ma comunque: come fai a sapere se verrà esposto o meno da chiunque voglia utilizzare il codice che hai pubblicato qui?
Il tuo buon senso

Quindi è responsabilità di un poster delineare ogni potenziale utilizzo del codice o ogni fonte per argomenti? Forse ho aspettative più alte delle persone. Sarebbe più felice se aggiungessi una nota per non consentire all'utente di accedere $tableName?
Lee,

È responsabilità di un poster pubblicare un codice affidabile, se il loro intento è aiutare qualcuno, non solo mettersi in mostra.
Il tuo buon senso

1

Ecco un'altra soluzione (sottile) per questo problema:

All'inizio devi contare i dati dell'array di origine (qui: $ aData) con count (). Quindi usi array_fill () e generi un nuovo array che contiene tutte le voci dell'array di origine, ognuna con il valore "(?,?)" (Il numero di segnaposto dipende dai campi che usi; qui: 2). Quindi l'array generato deve essere imploso e come colla viene utilizzata una virgola. All'interno del ciclo foreach, è necessario generare un altro indice relativo al numero di segnaposto utilizzati (numero di segnaposto * indice di array corrente + 1). È necessario aggiungere 1 all'indice generato dopo ogni valore associato.

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

0

Puoi inserire più righe in una singola query con questa funzione:

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 è una matrice di matrici di valori. Nel tuo caso chiameresti la funzione con

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

Questo ha il vantaggio di usare istruzioni preparate , mentre si inseriscono più righe con una singola query. Sicurezza!


0

Ecco la mia soluzione: https://github.com/sasha-ch/Aura.Sql basato sulla libreria auraphp / Aura.Sql.

Esempio di utilizzo:

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

I segnalazioni di bug sono i benvenuti.


A partire da 2.4 è possibile creare inserti multipli con github.com/auraphp/Aura.SqlQuery/tree/… e utilizzare ExtendedPdo per eseguire :).
Hari KT,

0

Il mio esempio nel mondo reale per inserire tutti i codici postali tedeschi in una tabella vuota (per aggiungere i nomi delle città in seguito):

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

Come puoi vedere è completamente flessibile. Non è necessario controllare la quantità di colonne o verificare la posizione della colonna. Hai solo bisogno di impostare i dati di inserimento:

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

Sono orgoglioso di alcuni costruttori di stringhe di query in quanto funzionano senza pesanti funzioni di array come array_merge. Soprattutto vsprintf () è stata una buona scoperta.

Alla fine ho dovuto aggiungere 2x while () per evitare di superare il limite di memoria. Questo dipende dal limite di memoria, ma è comunque una buona soluzione generale per evitare problemi (e avere 10 query è ancora molto meglio di 10.000).


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

}
?>

Benvenuto in StackOverflow. Non solo il codice, pubblica il tuo problema e spiega.
Prakash Palnati,

fondamentalmente. è solo un'implementazione del codice fornito nella risposta accettata
Your Common Sense

0

Ho avuto lo stesso problema ed è così che realizzo per me stesso e ho creato una funzione per me stesso (e puoi usarlo se questo ti aiuta).

Esempio:

INSERIRE in paesi (paese, città) VALORI (Germania, Berlino), (Francia, Parigi);

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

Se insertMultipleData ($ table, $ multi_params) restituisce TRUE , i tuoi dati sono stati inseriti nel tuo database.


0

Sulla base dei miei esperimenti, ho scoperto che l'istruzione di inserimento mysql con più righe di valore in una singola transazione è la più veloce.

Tuttavia, se i dati sono eccessivi, l' max_allowed_packetimpostazione di mysql potrebbe limitare l'inserimento della singola transazione con più righe di valore. Quindi, le seguenti funzioni falliranno quando ci sono dati maggiori della max_allowed_packetdimensione di mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

Il transactionSpeedmetodo di maggior successo nell'inserimento di enormi scenari di dati è il metodo, ma richiede più tempo rispetto ai metodi sopra menzionati. Quindi, per gestire questo problema, puoi dividere i tuoi dati in blocchi più piccoli e chiamare l'inserimento di singole transazioni più volte o rinunciare alla velocità di esecuzione utilizzandotransactionSpeed metodo.

Ecco la mia ricerca

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

I risultati per 100.000 voci per una tabella contenente solo due colonne sono i seguenti

$ 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

0

Questo ha funzionato per me

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

0

che dire qualcosa del genere:

        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'idea alla base di ciò è di scorrere i valori dell'array, aggiungendo "numeri ID" a ciascun loop per i segnaposto delle istruzioni preparate, mentre allo stesso tempo si aggiungono i valori all'array per i parametri di associazione. Se non ti piace usare l'indice "chiave" dall'array, puoi aggiungere $ i = 0 e $ i ++ all'interno del ciclo. Funziona in questo esempio, anche se si dispone di array associativi con chiavi denominate, funzionerebbe comunque a condizione che le chiavi fossero uniche. Con un po 'di lavoro andrebbe bene anche per le matrici nidificate.

** Si noti che il substr rimuove le variabili $ sql nell'ultimo spazio e virgola, se non si dispone di uno spazio, è necessario modificarlo in -1 anziché -2.


-1

La maggior parte delle soluzioni fornite qui per creare la query preparata sono più complesse di quanto debbano essere. Utilizzando le funzioni integrate di PHP è possibile creare facilmente l'istruzione SQL senza sovraccarico significativo.

Dato $records, un array di record in cui ogni record è esso stesso un array indicizzato (sotto forma di field => value), la seguente funzione inserirà i record nella tabella data $table, su una connessione PDO $connection, usando solo una singola istruzione preparata. Si noti che questa è una soluzione PHP 5.6+ a causa dell'utilizzo dell'argomento decompressione nella chiamata a 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);
}

1
Questo codice non dovrebbe mai essere usato in quanto vulnerabile all'iniezione SQL
Il tuo senso comune
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.