PDO Prepared Вставляет несколько строк в одном запросе

146

В настоящее время я использую этот тип SQL на MySQL, чтобы вставить несколько строк значений в один запрос:

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

В чтениях по PDO операторы, подготовленные к использованию, должны обеспечить мне большую безопасность, чем статические запросы.

Поэтому я хотел бы знать, возможно ли сгенерировать «вставку нескольких строк значений с помощью одного запроса» с использованием подготовленных операторов.

Если да, могу ли я узнать, как я могу это реализовать?

hoball
источник
осторожно с большим количеством ответов для $stmt->execute($data); php.net/manual/en/… В основном все параметры передаются проверенными как строки. Просто переберите данные после построения запроса и вручную bindValueили bindParamпередав тип в качестве третьего аргумента.
MrMesees

Ответы:

151

Вставка нескольких значений с подготовленными заявлениями PDO

Вставка нескольких значений в одном операторе выполнения. Почему, потому что согласно этой странице это быстрее, чем обычные вставки.

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

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

больше значений данных, или у вас, вероятно, есть цикл, который заполняет данные.

С подготовленными вставками вам нужно знать поля, в которые вы вставляете, и количество полей для создания? заполнители, чтобы связать ваши параметры.

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

Именно так мы хотим, чтобы оператор вставки выглядел так.

Теперь код:

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

Хотя в моем тесте разница была только в 1 секунду при использовании нескольких вставок и обычных подготовленных вставок с одним значением.

Герберт Балагтас
источник
4
Опечатка в приведенном выше объяснении упоминает $ datafields, хотя $ datafield используется в $ sql. Таким образом, копирование вставки приведет к ошибке. Пожалуйста, исправьте. Спасибо за это решение, хотя.
pal4life
1
Использовал это некоторое время, затем заметил, что значения с одинарными кавычками в них не экранированы должным образом. Использование двойных кавычек на имплозии работает для меня как прелесть: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman
1
array_merge кажется более дорогим, чем просто использование array_push.
K2xL
14
Когда вы говорите «разница была всего в 1 секунду», сколько строк данных вы вставляли? 1 секунда довольно значительна в зависимости от контекста.
Кевин Дайс
3
Оптимизация: нет смысла звонить placeholders()снова и снова. Вызовите его один раз перед циклом с sizeof($datafields)и добавьте строку результата $question_marks[]внутрь цикла.
AVIDeveloper
71

Тот же ответ, что и у мистера Балагтаса, чуть яснее ...

Последние версии MySQL и PHP PDO сделать поддержка нескольких строк INSERTзаявления.

Обзор SQL

SQL будет выглядеть примерно так, при условии, что вы хотите создать таблицу из 3 столбцов INSERT.

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

ON DUPLICATE KEY UPDATEработает как положено даже с многорядной INSERT; добавить это:

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

Обзор PHP

Ваш PHP-код будет следовать обычным вызовам $pdo->prepare($qry)и $stmt->execute($params)вызовам PDO.

$paramsбудет одномерным массивом всех значений, передаваемых в INSERT.

В приведенном выше примере он должен содержать 9 элементов; PDO будет использовать каждый набор из 3 как один ряд значений. (Вставка 3 строк по 3 столбца в каждом = 9 элементов массива.)

Реализация

Ниже код написан для ясности, а не эффективности. Работайте с array_*()функциями PHP, чтобы лучше отображать или просматривать ваши данные, если хотите. Можно ли использовать транзакции, очевидно, зависит от типа таблицы MySQL.

Предполагая, что:

  • $tblName - имя строки таблицы для вставки
  • $colNames- одномерный массив имен столбцов таблицы. Эти имена столбцов должны быть действительными идентификаторами столбцов MySQL; избегайте их с помощью галочек (``), если они не
  • $dataVals - многомерный массив, где каждый элемент представляет собой 1-й массив строки значений для INSERT

Образец кода

// 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
источник
6
Это очень плохо, что PDO справляется с этим, есть несколько очень элегантных способов сделать это в других драйверах БД.
Джонатон
Это устанавливает заполнители еще более кратко, делая $rowPlacesбольше не нужно:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Фил
Работает отлично. Я бы добавил к этому ответу необходимость обеспечения уникальности (комбинации) индексов в таблице. Как и в инструкции ALTER TABLE votesADD UNIQUE unique_index( user, email, address);
Джузеппе
1
Потрясающие! Кстати, использование array_push($dataToInsert, ...array_values($dataVals));будет намного быстрее, чемforeach ($dataVals as $row => $data) {}
Anis
39

Что бы это ни стоило, я видел, что многие пользователи рекомендуют выполнять итерации по операторам INSERT, а не строить их как однострочный запрос, как это сделал выбранный ответ. Я решил запустить простой тест с двумя полями и очень простым оператором вставки:

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

В то время как сам запрос занимал миллисекунды или меньше, последний (однострочный) запрос был последовательно в 8 раз быстрее или больше. Если бы это было сделано, чтобы отразить импорт тысяч строк во многих других столбцах, разница могла бы быть огромной.

JM4
источник
@ JM4 - отличная идея, чтобы поместить 10 строк непосредственно в одном исполнении . Но как я могу вставить тысячи строк, когда они хранятся в таком объекте, как JSON? Мой код ниже работает безупречно. Но как я могу настроить его, чтобы вставить 10 строк в одном исполнении? `foreach ($ json_content as $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [date]; $ row3 = $ datarow [row3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [row6]; $ row7 = $ datarow [row7]; // теперь выполняем $ databaseinsert-> execute (); } // конец foreach `
Питер
@ JM4 - ... и мой второй вопрос: "почему bind_paramво второй процедуре импорта нет операторов"?
Питер
Разве тебе не пришлось бы зацикливаться дважды? Вы также должны были бы динамически генерировать (?,?), верно?
NoobishPro
@NoobishPro Да, вы можете использовать то же самое для / foreach для генерации обоих.
Чейзи Чаз
34

Принятый ответ Герберта Балагтаса хорошо работает, когда массив $ data мал. При больших массивах $ data функция array_merge становится слишком медленной. Мой тестовый файл для создания массива $ data имеет 28 столбцов и около 80 000 строк. Окончательный сценарий занял 41 с.

Использование array_push () для создания $ insert_values ​​вместо array_merge () привело к ускорению в 100 раз со временем выполнения 0,41 с .

Проблемный array_merge ():

$insert_values = array();

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

Чтобы устранить необходимость в array_merge (), вместо этого вы можете создать следующие два массива:

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

Эти массивы могут затем использоваться следующим образом:

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
В PHP 5.6 вы можете сделать array_push($data, ...array_values($row))вместо $data = array_merge($data, array_values($row));. Намного быстрее.
mpen
Почему 5,6? Документация ничего не говорит о 5.6, array_push()доступна даже в php 4.
ZurabWeb
1
@Piero - это только PHP 5.6+ код не из-за использования array_push(), а потому что @Mark использует распаковку аргументов. Заметили ...array_values()звонок там?
mariano.iglesias
@ mariano.iglesias array_values()также доступен в php 4. Не уверен, что под этим вы подразумеваете argument unpacking.
ZurabWeb
2
@Piero, Распаковка аргументов - это функция, представленная в PHP 5.6. Это способ предоставления нескольких аргументов в виде массива. Проверьте здесь - php.net/manual/en/…
Анис
14

Два возможных подхода:

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

Или:

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

Если данные для всех строк находятся в одном массиве, я бы использовал второе решение.

Zyx
источник
10
в последнем случае вы не делаете несколько (возможно, тысячи) отдельных вызовов выполнения вместо того, чтобы объединять их в один оператор?
JM4
@ JM4, ты предлагаешь $stmt->execute();быть вне цикла foreach?
Bafromca
@bafromca - Да, я. Смотрите мой ответ выше с upvotes. На чистом операторе вставки нет никакой причины, по которой я могу логически придумать, что это не может быть одно утверждение. Один вызов, один выполнить. Фактически, мой ответ с начала 2012 года может быть улучшен еще больше - то, что я сделаю позже, когда у меня будет больше времени. Если вы начинаете добавлять комбинации вставки / обновления / удаления, это уже другая история.
JM4
12

Это просто не то, как вы используете готовые заявления.

Вполне нормально вставлять одну строку в запрос, потому что вы можете выполнить один подготовленный оператор несколько раз с разными параметрами. Фактически, это одно из величайших преимуществ, поскольку оно позволяет вам эффективно и безопасно вставлять большое количество строк.

Поэтому, возможно, можно реализовать предложенную вами схему, по крайней мере, для фиксированного числа строк, но почти гарантировано, что это не совсем то, что вы хотите.

sebasgo
источник
1
Можете ли вы предложить лучший способ вставить несколько строк в таблицу?
Crashthatch
@Crashthatch: просто сделайте это наивным способом: настройте подготовленный оператор один раз, затем выполните его для каждой строки с различными значениями для связанных параметров. Это второй подход в ответе Зика.
Себасго
2
Цель, которую вы упомянули для подготовленного заявления, является правильной. Но использование мультиинсертирования - это еще один метод повышения скорости вставки, который также можно использовать с подготовленным оператором. По моему опыту, при переносе 30 миллионов данных строк с использованием подготовленного оператора PDO я видел, что мульти-вставка была в 7-10 раз быстрее, чем сгруппированная одиночная вставка в транзакциях.
Анис
1
Абсолютно согласен с Анисом. У меня 100 тыс. Строк, и я получаю огромное увеличение скорости благодаря многорядным вставкам.
Кеннет
Утверждать, что вызов реляционной базы данных в цикле один раз для каждой строки, как правило, хорошо, с чем я не согласен. Даунвот за это. Конечно, иногда это нормально. Я не верю в абсолют в инженерном деле. Но это анти-шаблон, который следует использовать только в некоторых случаях.
Брэндон
8

Более короткий ответ: сгладьте массив данных, упорядоченный по столбцам, затем

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

При вставке 1000 записей или около того вам не нужно циклически просматривать каждую запись, чтобы вставить их, когда все, что вам нужно, - это подсчет значений.

fyrye
источник
5

Вот мой простой подход.

    $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
Вы побеждаете смысл использования готовых заявлений. On the readings on PDO, the use prepared statements should give me a better security than static queries.
Оператор
2
Просто изображение, которое вы не подтвердили $workouts_id, которое может иметь $values с довольно неожиданными данными. Вы не можете гарантировать, что, возможно, не сейчас, но в будущем другой разработчик сделает эти данные незащищенными. Поэтому я думаю, что более правильно сделать запрос, подготовленный PDO.
Nikita_kharkov_ua
3

Вот класс, который я написал, чтобы сделать несколько вставок с опцией очистки:

<?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;
    }
}
Пьер Дюмуид
источник
Привет Пьер. Может быть, вы не активны здесь больше. Тем не менее, я просто хотел отметить, что моя идея по этому вопросу выглядит почти идентично вашей. Чистое совпадение, так как я полагаю, в этом нет ничего особенного. Я также добавил классы для операций DELETE- и UPDATE-Operations и затем включил некоторые идеи. Я просто не видел твой класс. Пожалуйста, извините за мою бесстыдную саморекламу, но я думаю, что это кому-то поможет. Надеюсь, это не против SO-правил. Найдите это здесь .
JackLeEmmerdeur
1

Вот как я это сделал:

Сначала определите имена столбцов, которые вы будете использовать, или оставьте это поле пустым, и pdo предположит, что вы хотите использовать все столбцы таблицы - в этом случае вам нужно будет сообщить значения строк в точном порядке, в котором они отображаются в таблице. ,

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

Теперь предположим, что у вас уже есть двухмерный массив. Итерируйте его и создайте строку с вашими значениями строки, как таковые:

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

Теперь, что вы только что сделали, это проверили, определено ли уже $ row, и, если нет, создайте его и сохраните значения строк и необходимый синтаксис SQL, чтобы он был действительным оператором. Обратите внимание, что строки должны заключаться в двойные и одинарные кавычки, поэтому они будут быстро распознаваться как таковые.

Все, что осталось сделать, это подготовить оператор и выполнить так:

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

До сих пор тестировалось до 2000 строк, и время выполнения мрачное. Проведу еще несколько тестов и вернусь сюда, если у меня будет что-то еще.

С уважением.

Тео Т. Карранса
источник
1

Поскольку это еще не было предложено, я уверен, что LOAD DATA INFILE по-прежнему является самым быстрым способом загрузки данных, поскольку он отключает индексирование, вставляет все данные и затем повторно включает индексы - все в одном запросе.

Сохранение данных в формате csv должно быть довольно тривиальным, учитывая fputcsv. MyISAM работает быстрее, но вы все равно получаете большую производительность в InnoDB. Есть и другие недостатки, поэтому я бы пошел по этому пути, если вы вставляете много данных, и не беспокоитесь о менее чем 100 строках.

avatarofhope2
источник
1

Хотя старый вопрос, все вклады мне очень помогли, вот мое решение, которое работает в моем DbContextклассе. $rowsПараметр просто массив ассоциативных массивов , представляющих строку или модель: field name => insert value.

Если вы используете шаблон, который использует модели, он хорошо вписывается, когда данные модели передаются в виде массива, скажем, из ToRowArrayметода в классе модели.

Примечание . Само собой разумеется, но никогда не допускайте, чтобы аргументы, передаваемые этому методу, были доступны пользователю или зависели от любого пользовательского ввода, кроме значений вставки, которые были проверены и очищены. $tableNameАргумент и имена столбцов должны определяться логикой вызова; например, Userмодель может быть сопоставлена ​​с пользовательской таблицей, в которой список столбцов сопоставлен с полями элементов модели.

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();
}
подветренный
источник
избавиться от транзакции, так как нет смысла использовать ее для одного запроса. и, как обычно, этот код уязвим для внедрения SQL или ошибки запроса.
Твой здравый смысл
Вы правы насчет избыточного использования транзакций в этом случае, но я не понимаю, насколько это уязвимо для внедрения SQL. Он параметризован, так что я могу только предположить, что вы предполагаете $tableName, что он открыт для пользователя, а это не так, он в DAL. Можете ли вы расширить свои претензии? Бесполезно просто говорить вещи.
Ли
ну, это не только имя таблицы, но в любом случае: как вы можете узнать, будет ли она раскрыта кем-либо, кто будет использовать код, который вы разместили здесь?
Твой здравый смысл
Таким образом, ответственность автора заключается в том, чтобы обрисовать в общих чертах каждое потенциальное использование кода или каждого источника аргументов? Может быть, у меня есть более высокие ожидания людей. Будет ли это счастливее, если я добавлю заметку, чтобы пользователь не имел доступа к ней $tableName?
Ли
Постер несет ответственность за публикацию надежного кода, если его цель - помочь кому-то, а не просто похвастаться.
Твой здравый смысл
1

Вот еще одно (тонкое) решение этой проблемы:

Сначала вам нужно посчитать данные исходного массива (здесь: $ aData) с помощью count (). Затем вы используете array_fill () и генерируете новый массив с таким количеством записей, как у исходного массива, каждый со значением «(?,?)» (Количество заполнителей зависит от используемых вами полей; здесь: 2). Затем сгенерированный массив нужно развернуть и в качестве клея использовать запятую. В цикле foreach вам нужно сгенерировать еще один индекс относительно количества используемых заполнителей (количество заполнителей * текущий индекс массива + 1). Вам нужно добавить 1 к сгенерированному индексу после каждого связанного значения.

$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

Вы можете вставить несколько строк в один запрос с помощью этой функции:

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 это массив массивов значений. В вашем случае вы бы вызвали функцию с

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

Преимущество заключается в том, что вы используете подготовленные операторы , вставляя несколько строк одним запросом. Безопасность!

Крис Михаэлидис
источник
0

Вот мое решение: https://github.com/sasha-ch/Aura.Sql на основе библиотеки auraphp / Aura.Sql.

Пример использования:

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

Сообщения об ошибках приветствуются.

саша-ч
источник
Начиная с версии 2.4 вы можете создавать мульти-вставки с помощью github.com/auraphp/Aura.SqlQuery/tree/… и использовать ExtendedPdo для выполнения :).
Хари К.Т.
0

Мой пример из реального мира, чтобы вставить все немецкие почтовые индексы в пустую таблицу (чтобы добавить названия городов позже):

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

Как вы можете видеть его полностью гибким. Вам не нужно проверять количество столбцов или проверять, в какой позиции находится ваш столбец. Вам нужно только установить данные вставки:

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

Я горжусь некоторыми конструкторами строк запроса, поскольку они работают без тяжелых функций массива, таких как array_merge. Особенно vsprintf () была хорошей находкой.

Наконец, мне нужно было добавить 2x while (), чтобы избежать превышения лимита памяти. Это зависит от вашего лимита памяти, но в целом это хорошее общее решение, позволяющее избежать проблем (а наличие 10 запросов по-прежнему намного лучше, чем 10.000).

mgutt
источник
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
источник
Добро пожаловать в stackoverflow. Не просто код, пожалуйста, напишите, в чем ваша проблема и объясните.
Пракаш Палнати
в принципе. это всего лишь реализация кода, приведенного в принятом ответе
Ваш здравый смысл
0

У меня была та же проблема, и вот как я выполняю для себя, и я сделал для себя функцию для нее (и вы можете использовать ее, если это поможет вам).

Пример:

INSERT INTO страны (страна, город) ЦЕННОСТИ (Германия, Берлин), (Франция, Париж);

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

Если insertMultipleData ($ table, $ multi_params) возвращает TRUE , ваши данные были вставлены в вашу базу данных.

Dardan
источник
0

Основываясь на своих экспериментах, я обнаружил, что оператор вставки mysql с несколькими строками значений в одной транзакции является самым быстрым.

Однако, если данных слишком много, тогда max_allowed_packetнастройка mysql может ограничить вставку одной транзакции несколькими строками значений. Следовательно, следующие функции потерпят неудачу, когда есть данные, превышающие max_allowed_packetразмер mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

Наиболее успешным в сценарии вставки огромных данных является transactionSpeedметод, но он требует больше времени, чем вышеупомянутые методы. Таким образом, для решения этой проблемы вы можете либо разбить ваши данные на более мелкие порции и вызвать одну транзакцию вставки несколько раз, либо отказаться от скорости выполнения с помощьюtransactionSpeed метода.

Вот мое исследование

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

Результаты для 100 000 записей для таблицы, содержащей только два столбца, приведены ниже

$ 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
источник
0

Это сработало для меня

$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

что-то вроде этого:

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

Идея заключается в том, чтобы циклически проходить через значения вашего массива, добавляя «номера идентификаторов» в каждый цикл для заполнителей заполненных вами операторов, в то же время вы добавляете значения в свой массив для параметров привязки. Если вам не нравится использовать индекс «key» из массива, вы можете добавить $ i = 0 и $ i ++ внутри цикла. В этом примере любой из них работает, даже если у вас есть ассоциативные массивы с именованными ключами, он все равно будет работать, если ключи будут уникальными. С небольшой работой это было бы хорошо и для вложенных массивов ..

** Обратите внимание, что substr удаляет переменные $ sql с последним пробелом и запятой, если у вас нет пробела, вам нужно изменить это значение на -1, а не на -2.

Дин Уильямс
источник
-1

Большинство решений, приведенных здесь для создания подготовленного запроса, являются более сложными, чем они должны быть. Используя встроенные функции PHP, вы можете легко создавать оператор SQL без значительных накладных расходов.

Для данного $recordsмассива записей, где каждая запись сама является индексированным массивом (в форме field => value), следующая функция вставит записи в данную таблицу $tableпри подключении к PDO $connection, используя только один подготовленный оператор. Обратите внимание, что это решение PHP 5.6+ из-за использования распаковки аргументов при вызове 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
источник
1
Этот код никогда не должен использоваться, так как он уязвим для SQL-инъекций
ваш здравый смысл