PDO Prepared Inserta múltiples filas en una sola consulta

146

Actualmente estoy usando este tipo de SQL en MySQL para insertar varias filas de valores en una sola consulta:

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

En las lecturas de PDO, las declaraciones de uso preparadas deberían darme una mejor seguridad que las consultas estáticas.

Por lo tanto, me gustaría saber si es posible generar "insertar múltiples filas de valores mediante el uso de una consulta" utilizando declaraciones preparadas.

En caso afirmativo, ¿puedo saber cómo puedo implementarlo?

hoball
fuente
cuidado con muchas de las respuestas para $stmt->execute($data); php.net/manual/en/… Básicamente todos los parámetros se pasan validados como cadenas. Simplemente recorra los datos después de crear la consulta y escriba manualmente bindValueo bindParampase el tipo como tercer argumento.
MrMesees

Respuestas:

151

Insertar valores múltiples con declaraciones preparadas de DOP

Insertar múltiples valores en una declaración de ejecución. ¿Por qué porque de acuerdo con esta página es más rápido que las inserciones regulares?

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

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

más valores de datos o probablemente tenga un bucle que complete los datos.

Con las inserciones preparadas, necesita saber los campos en los que está insertando y la cantidad de campos para crear el? marcadores de posición para vincular sus parámetros.

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

Así es básicamente cómo queremos que se vea la declaración de inserción.

Ahora, el código:

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

Aunque en mi prueba, solo hubo una diferencia de 1 segundo al usar insertos múltiples e insertos preparados regularmente con un solo valor.

Herbert Balagtas
fuente
44
Un error tipográfico, en la explicación anterior, menciona $ datafields aunque $ datafield se usa en $ sql. Por lo tanto, copiar y pegar provocaría un error. Por favor rectifique. Gracias por esta solución sin embargo.
pal4life
1
Usé esto por un tiempo y luego noté que los valores con comillas simples no se escapan correctamente. Usar comillas dobles en implosión funciona como un encanto para mí: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman
1
array_merge parece más costoso que simplemente usar un array_push.
K2xL
14
Cuando dice "solo había una diferencia de 1 segundo", ¿cuántas filas estaba insertando un dato? 1 segundo es bastante significativo dependiendo del contexto.
Kevin Dice
3
Optimización: No tiene sentido llamar placeholders()una y otra vez. Llámelo una vez antes del ciclo con sizeof($datafields)y agregue la cadena de resultados al $question_marks[]interior del ciclo.
AVIDeveloper
71

La misma respuesta que el Sr. Balagtas, un poco más claro ...

Las versiones recientes de MySQL y PHP DOP hacen de soporte de varias filas INSERTdeclaraciones.

Descripción general de SQL

El SQL se verá más o menos así, suponiendo una tabla de 3 columnas que le gustaría INSERTtener.

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

ON DUPLICATE KEY UPDATEfunciona como se espera incluso con un INSERT de varias filas; agregue esto:

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

Descripción general de PHP

Su código PHP seguirá lo habitual $pdo->prepare($qry)$stmt->execute($params) llamadas y PDO.

$paramsserá una matriz unidimensional de todos los valores para pasar a INSERT.

En el ejemplo anterior, debe contener 9 elementos; PDO usará cada conjunto de 3 como una sola fila de valores. (Insertar 3 filas de 3 columnas cada una = matriz de 9 elementos).

Implementación

El siguiente código está escrito para mayor claridad, no eficiencia. Trabaja con PHParray_*() funciones de para obtener mejores formas de asignar o recorrer sus datos si lo desea. Si puede usar transacciones obviamente depende de su tipo de tabla MySQL.

Asumiendo:

  • $tblName - el nombre de cadena de la tabla para INSERTAR
  • $colNames- Matriz unidimensional de los nombres de columna de la tabla Estos nombres de columna deben ser identificadores de columna MySQL válidos; escapar de ellos con backticks (``) si no están
  • $dataVals - matriz multidimensional, donde cada elemento es una matriz 1-d de una fila de valores para INSERTAR

Código de muestra

// 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
fuente
66
Eso es realmente una lástima que PDO lo maneje de esta manera, hay algunas formas muy elegantes de hacerlo en otros controladores de DB.
Jonathon
Esto configura los marcadores de posición aún más tersamente, lo que hace que $rowPlacesya no sea necesario:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil
Funciona perfecto Añadiría a esta respuesta la necesidad de garantizar la unicidad de los (combinación de) índices en la tabla. Al igual que en ALTER TABLE votesADD UNIQUE unique_index( user, email, address);
Giuseppe
1
¡Increíble! Por cierto, el uso array_push($dataToInsert, ...array_values($dataVals));será mucho más rápido entoncesforeach ($dataVals as $row => $data) {}
Anis
39

Por lo que vale, he visto que muchos usuarios recomiendan iterar a través de las instrucciones INSERT en lugar de construir como una sola consulta de cadena como lo hizo la respuesta seleccionada. Decidí ejecutar una prueba simple con solo dos campos y una instrucción de inserción muy básica:

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

Si bien la consulta general en sí misma tomó milisegundos o menos, la última consulta (cadena única) fue consistentemente 8 veces más rápida o más. Si esto se construyó para decir que refleja una importación de miles de filas en muchas columnas más, la diferencia podría ser enorme.

JM4
fuente
@ JM4: gran idea para poner 10 filas directamente en una ejecución . Pero, ¿cómo puedo insertar miles de filas cuando se almacenan en un objeto como JSON? Mi código a continuación funciona de manera perfecta. Pero, ¿cómo puedo ajustarlo para insertar 10 filas en una ejecución? `foreach ($ json_content como $ datarow) {$ id = $ datarow [id]; $ fecha = $ datarow [fecha]; $ fila3 = $ datarow [fila3]; $ fila4 = $ datarow [fila4]; $ fila5 = $ datarow [fila5]; $ fila6 = $ datarow [fila6]; $ fila7 = $ datarow [fila7]; // ahora ejecuta $ databaseinsert-> execute (); } // fin de foreach `
Peter
@ JM4 - ... y mi segunda pregunta es: "¿por qué no hay una bind_paramdeclaración en la segunda rutina de importación"?
Peter
¿No tendrías que recorrer dos veces? También tendrías que generar dinámicamente el (?,?), ¿verdad?
NoobishPro
@NoobishPro Sí, puede usar lo mismo para / foreach para generar ambos.
Chazy Chaz
34

La respuesta aceptada de Herbert Balagtas funciona bien cuando la matriz $ data es pequeña. Con matrices $ data más grandes, la función array_merge se vuelve prohibitivamente lenta. Mi archivo de prueba para crear la matriz $ data tiene 28 cols y tiene aproximadamente 80,000 líneas. El guión final tardó 41 segundos en completarse.

El uso de array_push () para crear $ insert_values ​​en lugar de array_merge () resultó en una velocidad 100X con un tiempo de ejecución de 0.41s .

La problemática array_merge ():

$insert_values = array();

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

Para eliminar la necesidad de array_merge (), puede crear los siguientes dos arreglos en su lugar:

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

Estas matrices se pueden usar de la siguiente manera:

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.
fuente
44
En PHP 5.6 puedes hacer en array_push($data, ...array_values($row))lugar de $data = array_merge($data, array_values($row));. Mucho mas rápido.
mpen
¿Por qué 5.6? La documentación no dice nada sobre 5.6, array_push()está disponible incluso en php 4.
ZurabWeb
1
@Piero es solo PHP 5.6+ código no por el uso de array_push(), sino porque @Mark está usando el desempaquetado de argumentos. ¿Notaste la ...array_values()llamada allí?
mariano.iglesias
@ mariano.iglesias también array_values()está disponible en php 4. No estoy seguro si eso es lo que quiere decir argument unpacking.
ZurabWeb
2
@Piero, el desempaquetado de argumentos es una característica introducida en PHP 5.6. Es una forma de proporcionar múltiples argumentos como una matriz. Marque aquí - php.net/manual/en/…
Anis
14

Dos posibles enfoques:

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

Si los datos de todas las filas están en una sola matriz, usaría la segunda solución.

Zyx
fuente
10
en este último, ¿no estás haciendo varias (posiblemente miles) de llamadas de ejecución separadas en lugar de combinarlas en una sola declaración?
JM4
@ JM4, ¿sugiere que $stmt->execute();debería estar fuera del bucle foreach?
bafromca
@bafromca - Sí, lo soy. Vea mi respuesta arriba con votos a favor. En una declaración de inserción pura no hay ninguna razón por la que pueda llegar lógicamente a que no pueda ser una sola declaración. Una llamada, una ejecución. De hecho, mi respuesta de principios de 2012 podría mejorarse aún más, algo que haré más adelante cuando tenga más tiempo. Si comienza a agregar combinaciones Insertar / actualizar / eliminar, esa es una historia diferente.
JM4
12

Esa no es simplemente la forma en que usa las declaraciones preparadas.

Está perfectamente bien insertar una fila por consulta porque puede ejecutar una instrucción preparada varias veces con diferentes parámetros. De hecho, esa es una de las mayores ventajas, ya que le permite insertar una gran cantidad de filas de manera eficiente, segura y cómoda.

Por lo tanto, es posible implementar el esquema que propone, al menos para un número fijo de filas, pero está casi garantizado que esto no es realmente lo que desea.

sebasgo
fuente
1
¿Puede sugerir una mejor manera de insertar varias filas en una tabla?
Crashthatch
@Crashthatch: simplemente hágalo de la manera ingenua: configure la instrucción preparada una vez, luego ejecútela para cada fila con diferentes valores para los parámetros enlazados. Ese es el segundo enfoque en la respuesta de Zyk.
sebasgo
2
El propósito que mencionó para la declaración preparada es correcto. Pero, el uso de inserción múltiple es otra técnica para mejorar la velocidad de inserción y también se puede usar con una declaración preparada. En mi experiencia, mientras migraba 30 millones de datos de fila usando la declaración preparada de PDO, vi que la inserción múltiple era 7-10 veces más rápida que la inserción individual agrupada en las transacciones.
Anis
1
Absolutamente de acuerdo con Anis. Tengo 100k filas y obtengo un gran aumento de velocidad con inserciones de filas de muli.
Kenneth
Afirmar que llamar a una base de datos relacional en un ciclo una vez por fila es generalmente algo bueno, es algo con lo que no puedo estar de acuerdo. Voto por eso. De acuerdo, a veces está bien. No creo en los absolutos con la ingeniería. Pero este es un antipatrón que debe usarse solo en casos seleccionados.
Brandon
8

Una respuesta más corta: aplanar la matriz de datos ordenados por columnas y luego

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

Al insertar alrededor de 1,000 registros, no desea tener que recorrer cada registro para insertarlos cuando todo lo que necesita es un recuento de los valores.

fyrye
fuente
5

Aquí está mi enfoque 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();

fuente
66
estás derrotando el punto de usar declaraciones preparadas. El operador está preocupado por la seguridad en la preguntaOn the readings on PDO, the use prepared statements should give me a better security than static queries.
YesItsMe
2
Solo imágenes que no has validado $workouts_id, que pueden tener $values con datos bastante inesperados. No puede garantizar que tal vez no ahora, pero en el futuro otro desarrollador haga que estos datos sean inseguros. Así que creo que es más correcto hacer la consulta preparada por PDO.
Nikita_kharkov_ua
3

Aquí hay una clase que escribí hacer múltiples inserciones con la opción de purga:

<?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
fuente
Hola pierre Quizás ya no estés activo por aquí. Sin embargo, solo quería señalar que mi idea para este problema es casi idéntica a la suya. Pura coincidencia, como supongo que no hay mucho más que esto. Agregué clases para BORRAR Y ACTUALIZAR también, e involucré algunas ideas de aquí, después. Simplemente no vi tu clase. Disculpe mi descarada autopromoción aquí, pero supongo que será de ayuda para alguien. Espero que esto no esté en contra de las reglas SO. Encuéntralo aquí .
JackLeEmmerdeur
1

Así es como lo hice:

Primero defina los nombres de columna que usará, o déjelo en blanco y pdo asumirá que desea usar todas las columnas de la tabla, en cuyo caso deberá informar los valores de las filas en el orden exacto en que aparecen en la tabla .

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

Ahora, suponga que ya tiene una matriz bidimensional preparada. Iterarlo y construir una cadena con sus valores de fila, como tal:

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

Ahora, lo que acaba de hacer es verificar si $ filas ya estaba definido, y si no, crearlo y almacenar valores de fila y la sintaxis SQL necesaria para que sea una declaración válida. Tenga en cuenta que las cadenas deben ir entre comillas dobles y comillas simples, por lo que se reconocerán rápidamente como tales.

Todo lo que queda por hacer es preparar la declaración y ejecutarla como tal:

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

Probado con hasta 2000 filas hasta ahora, y el tiempo de ejecución es pésimo. Ejecutaré algunas pruebas más y volveré aquí en caso de que tenga algo más que aportar.

Saludos.

Théo T. Carranza
fuente
1

Como aún no se ha sugerido, estoy bastante seguro de que LOAD DATA INFILE sigue siendo la forma más rápida de cargar datos, ya que deshabilita la indexación, inserta todos los datos y luego vuelve a habilitar los índices, todo en una sola solicitud.

Guardar los datos como un csv debería ser bastante trivial teniendo en cuenta fputcsv. MyISAM es el más rápido, pero aún obtienes un gran rendimiento en InnoDB. Sin embargo, hay otras desventajas, por lo que seguiría esta ruta si está insertando muchos datos y no se molesta con menos de 100 filas.

avatarofhope2
fuente
1

Aunque una vieja pregunta, todas las contribuciones me ayudaron mucho, así que aquí está mi solución, que funciona dentro de mi propia DbContextclase. El $rowsparámetro es simplemente una matriz de matrices asociativas que representan filas o modelos:field name => insert value .

Si usa un patrón que usa modelos, esto encaja perfectamente cuando se pasan los datos del modelo como una matriz, digamos desde un ToRowArraymétodo dentro de la clase de modelo.

Nota : Debe ser evidente, pero nunca permita que los argumentos pasados ​​a este método se expongan al usuario o dependan de cualquier entrada del usuario, que no sean los valores de inserción, que han sido validados y desinfectados. El $tableNameargumento y los nombres de las columnas deben estar definidos por la lógica de llamada; por ejemplo, un Usermodelo podría asignarse a la tabla de usuario, que tiene su lista de columnas asignada a los campos miembros del modelo.

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();
}
Sotavento
fuente
deshacerse de una transacción, ya que no tiene sentido usar una para una sola consulta. y como de costumbre, este código es vulnerable a la inyección de SQL o error de consulta.
Su sentido común
Tiene razón sobre el uso redundante de transacciones para este caso, pero no veo cómo esto es vulnerable a la inyección de SQL. Está parametrizado, por lo que solo puedo suponer que está asumiendo que $tableNameestá expuesto al usuario, lo que no es así, está en el DAL. ¿Puedes ampliar tus reclamos? No es útil solo decir cosas.
Lee
bueno, no es solo un nombre de tabla, sino de todos modos: ¿cómo puede saber si estará expuesto o no por alguien que usaría el código que publicó aquí?
Su sentido común
Entonces, ¿es responsabilidad del afiche esbozar cada uso potencial del código o cada fuente de argumentos? Tal vez tengo mayores expectativas de las personas. ¿Te haría más feliz si añadiera una nota para no permitir que el usuario tenga acceso $tableName?
Lee
Es responsabilidad del afiche publicar un código confiable, si su intención es ayudar a alguien, no solo alardear.
Su sentido común
1

Aquí hay otra solución (delgada) para este problema:

Al principio, necesita contar los datos de la matriz fuente (aquí: $ aData) con count (). Luego usa array_fill () y genera una nueva matriz con tantas entradas como la matriz fuente tiene, cada una con el valor "(?,?)" (El número de marcadores de posición depende de los campos que utilice; aquí: 2). Entonces la matriz generada necesita ser implosionada y como pegamento se usa una coma. Dentro del ciclo foreach, debe generar otro índice con respecto al número de marcadores de posición que utiliza (número de marcadores de posición * índice de matriz actual + 1). Debe agregar 1 al índice generado después de cada valor enlazado.

$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
fuente
0

Puede insertar varias filas en una sola consulta con esta función:

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 es una matriz de matrices de valores. En su caso, llamaría a la función con

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

Esto tiene la ventaja de que utiliza declaraciones preparadas , al insertar varias filas con una sola consulta. ¡Seguridad!

Chris Michaelides
fuente
0

Mi ejemplo del mundo real para insertar todos los códigos postales alemanes en una tabla vacía (para agregar nombres de ciudades más adelante):

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

Como puede ver, es totalmente flexible. No necesita verificar la cantidad de columnas ni verificar en qué posición se encuentra su columna. Solo necesita configurar los datos de inserción:

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

Estoy orgulloso de algunos de los constructores de cadenas de consulta, ya que funcionan sin funciones de matriz pesadas como array_merge. Especialmente vsprintf () fue un buen hallazgo.

Finalmente, necesitaba agregar 2x while () para evitar exceder el límite de memoria. Esto depende de su límite de memoria, pero es una buena solución general para evitar problemas (y tener 10 consultas sigue siendo mucho mejor que 10.000).

mgutt
fuente
0

prueba.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
fuente
Bienvenido a stackoverflow. No solo el código, publique cuál es su problema y explíquelo.
Prakash Palnati
básicamente. es solo una implementación del código provisto en la respuesta aceptada
Your Common Sense
0

Tuve el mismo problema y así es como lo logro por mí mismo, e hice una función para mí (y puedes usarlo si eso te ayuda).

Ejemplo:

INSERTAR EN LOS VALORES de los países (país, ciudad) (Alemania, Berlín), (Francia, París);

$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 ($ tabla, $ multi_params) devuelve TRUE , sus datos se han insertado en su base de datos.

Dardan
fuente
0

En base a mis experimentos, descubrí que la instrucción de inserción mysql con filas de valores múltiples en una sola transacción es la más rápida.

Sin embargo, si los datos son demasiado, la max_allowed_packetconfiguración de mysql podría restringir la inserción de la transacción individual con varias filas de valores. Por lo tanto, las siguientes funciones fallarán cuando haya datos mayores que el max_allowed_packettamaño de mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

El más exitoso en el escenario de insertar grandes datos es el transactionSpeedmétodo, pero consume más tiempo que los métodos mencionados anteriormente. Entonces, para manejar este problema, puede dividir sus datos en fragmentos más pequeños y llamar a la inserción de transacción única varias veces o renunciar a la velocidad de ejecución mediante el transactionSpeedmétodo.

Aquí está mi investigación

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

Los resultados para 100,000 entradas para una tabla que contiene solo dos columnas son los siguientes

$ 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
fuente
0

Esto funciono para mi

$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
fuente
0

¿Qué tal algo como esto?

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

La idea detrás de esto es recorrer los valores de su matriz, agregando "números de identificación" a cada bucle para los marcadores de posición de la declaración preparada y, al mismo tiempo, agrega los valores a su matriz para los parámetros de enlace. Si no le gusta usar el índice "clave" de la matriz, puede agregar $ i = 0 y $ i ++ dentro del bucle. En este ejemplo, cualquiera de los dos funciona, incluso si tiene matrices asociativas con claves con nombre, funcionaría siempre que las claves fueran únicas. Con un poco de trabajo también estaría bien para matrices anidadas.

** Tenga en cuenta que substr quita las variables $ sql último espacio y coma, si no tiene un espacio necesitaría cambiar esto a -1 en lugar de -2.

Dean Williams
fuente
-1

La mayoría de las soluciones dadas aquí para crear la consulta preparada son más complejas de lo que deben ser. Usando las funciones integradas de PHP, puede crear fácilmente la declaración SQL sin una sobrecarga significativa.

Dado $records, una matriz de registros donde cada registro es en sí mismo una matriz indexada (en forma de field => value), la siguiente función insertará los registros en la tabla dada $table, en una conexión PDO $connection, usando solo una sola declaración preparada. Tenga en cuenta que esta es una solución PHP 5.6+ debido al uso de desempaquetado de argumentos en la llamada 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);
}
mariano.iglesias
fuente
1
Este código nunca debe usarse, ya que es vulnerable a la inyección de SQL
su sentido común el