DOP Preparado Insere várias linhas em uma única consulta

146

Atualmente, estou usando esse tipo de SQL no MySQL para inserir várias linhas de valores em uma única consulta:

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

Nas leituras da DOP, as instruções de uso preparadas devem me proporcionar uma segurança melhor do que as consultas estáticas.

Gostaria, portanto, de saber se é possível gerar "inserindo várias linhas de valores pelo uso de uma consulta" usando instruções preparadas.

Se sim, posso saber como implementá-lo?

Hoball
fonte
cuidado com muitas respostas para $stmt->execute($data); php.net/manual/en/… Basicamente, todos os parâmetros são passados ​​validados como strings. Basta percorrer os dados após criar a consulta e digitar manualmente bindValueou bindParampassar como terceiro argumento.
MrMesees 2/08

Respostas:

151

Inserção de múltiplos valores com instruções preparadas para DOP

Inserir vários valores em uma instrução de execução. Por que, de acordo com esta página , é mais rápido que as inserções regulares.

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

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

mais valores de dados ou você provavelmente tem um loop que preenche os dados.

Com inserções preparadas, você precisa conhecer os campos nos quais está inserindo e o número de campos para criar os? espaços reservados para vincular seus parâmetros.

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

É basicamente assim que queremos que a instrução insert seja semelhante.

Agora, o 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();

Embora no meu teste, houve apenas uma diferença de 1 segundo ao usar várias inserções e inserções regulares preparadas com valor único.

Herbert Balagtas
fonte
4
Um erro de digitação, na explicação acima, menciona $ datafields embora $ datafield seja usado em $ sql. Assim, copiar e colar resultaria em erro. Por favor, retifique. Obrigado por esta solução embora.
pal4life
1
Usei isso por um tempo e depois notei que os valores com aspas simples não são escapados corretamente. Usar aspas duplas na implosão funciona como um encanto para mim: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
Qwertzman
1
array_merge parece mais caro do que apenas usar um array_push.
K2xL
14
Quando você diz "houve apenas uma diferença de 1 segundo", quantas linhas os dados você estava inserindo? 1 segundo é bastante significativo, dependendo do contexto.
Kevin Dice
3
Otimização: Não faz sentido ligar placeholders()repetidamente. Chame-o uma vez antes do loop com sizeof($datafields)e anexe a sequência de resultados ao $question_marks[]interior do loop.
AVIDeveloper
71

Mesma resposta que o Sr. Balagtas, um pouco mais claro ...

As versões recentes do MySQL e PHP DOP fazer suporte multi-linha INSERTdeclarações.

Visão Geral do SQL

O SQL terá algo parecido com isto, assumindo uma tabela de 3 colunas que você gostaria INSERT.

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

ON DUPLICATE KEY UPDATEfunciona como esperado, mesmo com um INSERT com várias linhas; acrescente isto:

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

Visão Geral do PHP

Seu código PHP seguirá as chamadas usuais $pdo->prepare($qry)e do $stmt->execute($params)DOP.

$paramsserá uma matriz unidimensional de todos os valores a serem passados ​​para INSERT.

No exemplo acima, ele deve conter 9 elementos; O PDO usará cada conjunto de 3 como uma única linha de valores. (Inserir 3 linhas de 3 colunas cada = matriz de 9 elementos.)

Implementação

O código abaixo é escrito para maior clareza, não eficiência. Trabalhe com as array_*()funções PHP para obter melhores maneiras de mapear ou percorrer seus dados, se desejar. Se você pode usar transações obviamente depende do seu tipo de tabela MySQL.

Assumindo:

  • $tblName - o nome da string da tabela para INSERT
  • $colNames- Matriz unidimensional dos nomes das colunas da tabela Esses nomes de colunas devem ser identificadores válidos das colunas do MySQL; escapá-los com backticks (``) se não estiverem
  • $dataVals - matriz multidimensional, em que cada elemento é uma matriz 1-d de uma linha de valores para INSERT

Código de amostra

// 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
fonte
6
É realmente muito ruim que o PDO lide com isso dessa maneira; existem algumas maneiras muito elegantes de fazer isso em outros drivers de banco de dados.
Jonathon
Isso configura os espaços reservados ainda mais rapidamente, deixando de $rowPlacesser necessário:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil
Funciona perfeito. Eu acrescentaria a essa resposta a necessidade de garantir a exclusividade dos índices (combinação de) na tabela. Como em ALTER TABLE votesADD ORIGINAL unique_index( user, email, address);
Giuseppe
1
Impressionante! BTW, usando array_push($dataToInsert, ...array_values($dataVals));será muito mais rápido, entãoforeach ($dataVals as $row => $data) {}
Anis
39

Pelo que vale, tenho visto muitos usuários recomendando a iteração através de instruções INSERT, em vez de criar uma consulta de cadeia única como a resposta selecionada. Decidi executar um teste simples com apenas dois campos e uma instrução de inserção muito 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>";
?>

Enquanto a própria consulta geral levou milissegundos ou menos, a última (cadeia única) foi consistentemente 8 vezes mais rápida ou mais. Se isso foi criado para refletir uma importação de milhares de linhas em muito mais colunas, a diferença poderia ser enorme.

JM4
fonte
@ JM4 - ótima idéia para colocar 10 linhas diretamente em uma execução . Mas como posso inserir milhares de linhas quando elas são armazenadas em um objeto como JSON? Meu código abaixo funciona perfeitamente. Mas como posso ajustá-lo para inserir 10 linhas em uma execução? `foreach ($ json_content como $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [data]; $ row3 = $ datarow [linha3]; $ row4 = $ datarow [linha4]; $ row5 = $ datarow [linha5]; $ row6 = $ datarow [linha6]; $ row7 = $ datarow [linha7]; // agora execute $ databaseinsert-> execute (); } // fim do foreach `
Peter
@ JM4 - ... e minha segunda pergunta é: "por que não há nenhuma bind_paramdeclaração na segunda rotina de importação"?
Peter Peter
Você não teria que repetir duas vezes? Você também teria que gerar dinamicamente o (?,?), certo?
NoobishPro
@NoobishPro Sim, você pode usar o mesmo para / foreach para gerar os dois.
Chazy Chaz
34

A resposta aceita por Herbert Balagtas funciona bem quando a matriz de dados $ é pequena. Com matrizes de dados $ maiores, a função array_merge torna-se proibitivamente lenta. Meu arquivo de teste para criar a matriz de dados $ tem 28 colunas e tem cerca de 80.000 linhas. O script final levou 41s para ser concluído.

O uso de array_push () para criar $ insert_values ​​em vez de array_merge () resultou em uma velocidade de 100X com tempo de execução de 0,41s .

O problemático 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 a necessidade de array_merge (), você pode criar as duas matrizes a seguir:

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

Essas matrizes podem ser usadas da seguinte maneira:

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.
fonte
4
No PHP 5.6 você pode fazer em array_push($data, ...array_values($row))vez de $data = array_merge($data, array_values($row));. Muito mais rapido.
MPEN
Por que 5.6? Documentação não diz nada sobre 5.6, array_push()está disponível mesmo em php 4.
ZurabWeb
1
@Piero, é apenas o código PHP 5.6+, não por causa do uso de array_push(), mas porque o @Mark está usando o argumento de descompactação. Notou a ...array_values()ligação lá?
Mariano.iglesias
@ mariano.iglesias também array_values()está disponível no php 4. Não tenho certeza se é isso que você quer dizer com argument unpacking.
usar o seguinte texto
2
@Piero, a descompactação de argumentos é um recurso introduzido no PHP 5.6. É uma maneira de fornecer vários argumentos como uma matriz. Verifique aqui - php.net/manual/pt/…
Anis
14

Duas abordagens possíveis:

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

Se os dados de todas as linhas estiverem em uma única matriz, eu usaria a segunda solução.

Zyx
fonte
10
no último, você não está fazendo várias (possivelmente milhares) de chamadas de execução separadas, em vez de combinar em uma instrução?
JM4
@ JM4, você está sugerindo que $stmt->execute();deveria estar fora do loop foreach?
bafromca
@bafromca - Sim, eu sou. Veja minha resposta acima com votos positivos. Em uma instrução de inserção pura, não razão para logicamente sugerir que ela não pode ser uma única instrução. Uma chamada, uma execução. De fato, minha resposta do início de 2012 poderia ser melhorada ainda mais - algo que farei mais tarde quando tiver mais tempo. Se você começar a lançar combinações de Inserir / Atualizar / Excluir, essa é uma história diferente.
JM4 #
12

Simplesmente não é assim que você usa instruções preparadas.

Não há problema em inserir uma linha por consulta, porque você pode executar uma instrução preparada várias vezes com parâmetros diferentes. De fato, essa é uma das maiores vantagens, pois permite inserir um grande número de linhas de maneira eficiente, segura e confortável.

Portanto, talvez seja possível implementar o esquema que você propõe, pelo menos para um número fixo de linhas, mas é quase garantido que isso não é exatamente o que você deseja.

sebasgo
fonte
1
Você pode sugerir uma maneira melhor de inserir várias linhas em uma tabela?
Crashthatch
@Crashthatch: Faça da maneira ingênua: configure a instrução preparada uma vez e execute-a para cada linha com valores diferentes para os parâmetros vinculados. Essa é a segunda abordagem na resposta de Zyk.
sebasgo
2
O objetivo que você mencionou para a declaração preparada está correto. Porém, o uso da inserção múltipla é outra técnica para melhorar a velocidade da pastilha e também pode ser usada com instruções preparadas. Na minha experiência, ao migrar 30 milhões de dados de linha usando a declaração preparada pelo DOP, vi a inserção múltipla 7-10 vezes mais rápida do que a inserção única agrupada nas transações.
Anis
1
Concordo absolutamente com Anis. Eu tenho 100 mil linhas e recebo um enorme aumento de velocidade com inserções de várias linhas.
194 Kenneth
Afirmar que chamar um banco de dados relacional em um loop uma vez por linha geralmente é uma coisa boa, com algo que não posso concordar. Voto negativo para isso. Concedido, às vezes está tudo bem. Não acredito em absolutos com engenharia. Mas esse é um antipadrão que deve ser usado apenas em casos selecionados.
Brandon
8

Uma resposta mais curta: achatar a matriz de dados ordenada pelas colunas e depois

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

Ao inserir mais ou menos 1.000 registros, você não precisa percorrer todos os registros para inseri-los quando tudo o que você precisa é contar os valores.

fyrye
fonte
5

Aqui está minha abordagem simples.

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

fonte
6
você está derrotando o ponto de usar instruções preparadas. o op está preocupado com a segurança na questãoOn the readings on PDO, the use prepared statements should give me a better security than static queries.
YesItsMe
2
Apenas imagens que você não validou $workouts_id, o que pode ter $values com dados bastante inesperados. Você não pode garantir que talvez não agora, mas no futuro outro desenvolvedor torne esses dados inseguros. Então, acho muito mais correto fazer a consulta preparada pelo DOP.
Nikita_kharkov_ua
3

Aqui está uma classe que escrevi para fazer várias inserções com a opção de limpeza:

<?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
fonte
Olá Pierre. Talvez você não esteja mais ativo por aqui. No entanto, eu só queria ressaltar que minha ideia para esse problema parece quase idêntica à sua. Pura coincidência, como acho que não há muito mais para isso. Também adicionei aulas para as operações DELETE- E UPDATE- e envolvi algumas idéias daqui, posteriormente. Eu simplesmente não vi sua aula. Por favor, desculpe minha autopromoção desavergonhada aqui, mas acho que será de grande ajuda para alguém. Espero que isso não seja contra as regras do SO. Encontre aqui .
JackLeEmmerdeur
1

Foi assim que eu fiz:

Primeiro, defina os nomes das colunas que você usará ou deixe em branco e o pdo assumirá que você deseja usar todas as colunas da tabela. Nesse caso, você precisará informar os valores das linhas na ordem exata em que aparecem na tabela. .

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

Agora, suponha que você já tenha uma matriz bidimensional preparada. Itere-o e construa uma string com seus valores de linha, como:

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

Agora, o que você acabou de fazer foi verificar se $ linhas já estava definida e, se não, criar e armazenar valores de linha e a sintaxe SQL necessária para que seja uma instrução válida. Observe que as strings devem estar entre aspas duplas e aspas simples, para que sejam prontamente reconhecidas como tal.

Tudo o que resta fazer é preparar a instrução e executar, como tal:

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

Testado com até 2000 linhas até agora, e o tempo de execução é sombrio. Farei mais alguns testes e voltarei aqui caso eu tenha mais alguma coisa para contribuir.

Saudações.

Théo T. Carranza
fonte
1

Como ainda não foi sugerido, tenho certeza de que LOAD DATA INFILE ainda é a maneira mais rápida de carregar dados, pois desabilita a indexação, insere todos os dados e reativa os índices - tudo em uma única solicitação.

Salvar os dados como um CSV deve ser bastante trivial, tendo em mente o fputcsv. O MyISAM é mais rápido, mas você ainda obtém grande desempenho no InnoDB. Existem outras desvantagens, no entanto, eu seguiria essa rota se estiver inserindo muitos dados e não se incomodar com menos de 100 linhas.

avatarofhope2
fonte
1

Embora uma pergunta antiga todas as contribuições tenham me ajudado muito, aqui está a minha solução, que funciona dentro da minha própria DbContextclasse. O $rowsparâmetro é simplesmente uma matriz de matrizes associativas representando linhas ou modelos:field name => insert value .

Se você usar um padrão que usa modelos, isso se encaixa perfeitamente quando os dados do modelo são transmitidos como uma matriz, digamos de um ToRowArraymétodo dentro da classe de modelo.

Nota : Não é preciso dizer, mas nunca permita que os argumentos passados ​​para esse método sejam expostos ao usuário ou dependam de qualquer entrada do usuário, exceto os valores de inserção, que foram validados e higienizados. O $tableNameargumento e os nomes das colunas devem ser definidos pela lógica de chamada; por exemplo, um Usermodelo pode ser mapeado para a tabela de usuários, cuja lista de colunas é mapeada para os campos de membros do 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();
}
Lee
fonte
livrar-se de uma transação, pois não faz sentido usar uma para uma única consulta. e, como sempre, esse código é vulnerável a injeção de SQL ou erro de consulta.
Seu senso comum
Você está certo sobre o uso redundante de transações para este caso, mas não vejo como isso é vulnerável à injeção de SQL. É parametrizado para que eu possa presumir que você esteja $tableNameexposto ao usuário, o que não é, está no DAL. Você pode expandir suas reivindicações? Não é útil apenas dizer coisas.
19417 Lee
bem, não é apenas um nome de tabela, mas de qualquer maneira: como você pode saber se será exposto ou não por alguém que usaria o código que você postou aqui?
Seu senso comum
Portanto, é responsabilidade de um pôster descrever todo uso potencial do código ou toda fonte de argumentos? Talvez eu tenha maiores expectativas das pessoas. Você ficaria mais feliz se eu adicionasse uma nota para não permitir que o usuário tivesse acesso $tableName?
19417 Lee
É responsabilidade de um pôster publicar um código confiável, se a intenção deles é ajudar alguém, não apenas para mostrar.
Seu senso comum
1

Aqui está outra solução (reduzida) para esse problema:

Primeiro, você precisa contar os dados da matriz de origem (aqui: $ aData) com count (). Então você usa array_fill () e gera uma nova matriz com tantas entradas quanto a matriz de origem, cada uma com o valor "(?,?)" (O número de espaços reservados depende dos campos que você usa; aqui: 2). Em seguida, a matriz gerada precisa ser implodida e como cola é usada uma vírgula. No loop foreach, você precisa gerar outro índice com relação ao número de espaços reservados que você usa (número de espaços reservados * índice atual da matriz + 1). Você precisa adicionar 1 ao índice gerado após cada valor vinculado.

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

Você pode inserir várias linhas em uma única consulta com esta função:

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 é uma matriz de matrizes de valores. No seu caso, você chamaria a função com

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

Isso tem o benefício de você usar instruções preparadas , enquanto insere várias linhas com uma única consulta. Segurança!

Chris Michaelides
fonte
0

Aqui está a minha solução: https://github.com/sasha-ch/Aura.Sql base na biblioteca auraphp / Aura.Sql.

Exemplo de uso:

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

Relatórios de erros são bem-vindos.

sasha-ch
fonte
A partir do 2.4, você pode criar várias inserções com github.com/auraphp/Aura.SqlQuery/tree/… e usar o ExtendedPdo para executar :).
Hari KT
0

Meu exemplo do mundo real para inserir todos os códigos postais alemães em uma tabela vazia (para adicionar nomes de cidades posteriormente):

// 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 você pode ver, é totalmente flexível. Você não precisa verificar a quantidade de colunas ou em qual posição sua coluna está. Você só precisa definir os dados de inserção:

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

Estou orgulhoso de alguns dos construtores de string de consulta, pois eles funcionam sem funções de matriz pesadas, como array_merge. Especialmente vsprintf () foi uma boa descoberta.

Finalmente, eu precisei adicionar 2x while () para evitar exceder o limite de memória. Isso depende do seu limite de memória, mas é uma boa solução geral para evitar problemas (e ter 10 consultas ainda é muito melhor que 10.000).

mgutt
fonte
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
fonte
Bem-vindo ao stackoverflow. Não apenas o código, poste qual é o seu problema e explique.
Prakash Palnati
basicamente. é apenas uma implementação do código fornecido na resposta aceita
Seu senso comum
0

Eu tive o mesmo problema e é assim que realizo para mim mesmo e criei uma função para ele (e você pode usá-lo se isso o ajudar).

Exemplo:

INSERIR EM VALORES DE países (país, cidade) (Alemanha, Berlim), (França, 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;
    }

Se insertMultipleData ($ table, $ multi_params) retornar TRUE , seus dados foram inseridos no banco de dados.

Dardan
fonte
0

Com base em meus experimentos, descobri que a instrução de inserção do mysql com várias linhas de valor em uma única transação é a mais rápida.

No entanto, se os dados forem muitos, a max_allowed_packetconfiguração do mysql poderá restringir a inserção de transação única com várias linhas de valor. Portanto, as seguintes funções falharão quando houver dados maiores que o max_allowed_packettamanho do mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

O mais bem-sucedido no cenário de inserção de dados enormes é o transactionSpeedmétodo, mas consome mais tempo os métodos mencionados acima. Portanto, para lidar com esse problema, você pode dividir seus dados em pedaços menores e chamar a inserção de transação única várias vezes ou diminuir a velocidade de execução usandotransactionSpeed método

Aqui está minha pesquisa

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

Os resultados para 100.000 entradas para uma tabela que contém apenas duas colunas são os seguintes:

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

Isso funcionou para mim

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

que tal algo assim:

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

A idéia por trás disso é percorrer os valores da matriz, adicionando "números de identificação" a cada loop para os espaços reservados de instruções preparadas e, ao mesmo tempo, você adiciona os valores à matriz para os parâmetros de ligação. Se você não gosta de usar o índice "key" da matriz, pode adicionar $ i = 0 e $ i ++ dentro do loop. Ou funciona neste exemplo, mesmo se você tiver matrizes associativas com chaves nomeadas, ainda funcionará desde que as chaves sejam exclusivas. Com um pouco de trabalho, seria bom para matrizes aninhadas também ..

** Observe que substr retira as variáveis ​​$ sql do último espaço e vírgula, se você não tiver um espaço, precisará alterá-lo para -1 em vez de -2.

Dean Williams
fonte
-1

A maioria das soluções fornecidas aqui para criar a consulta preparada é mais complexa do que precisa. Usando as funções incorporadas do PHP, você pode criar facilmente a instrução SQL sem sobrecarga significativa.

Dado que $records, uma matriz de registros em que cada registro é uma matriz indexada (na forma de field => value), a função a seguir inserirá os registros na tabela fornecida $table, em uma conexão PDO $connection, usando apenas uma única instrução preparada. Observe que esta é uma solução PHP 5.6+ devido ao uso do argumento de descompactação na chamada para 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
fonte
1
Esse código nunca deve ser usado, pois é vulnerável à injeção de SQL
Seu senso comum