PDO Prepared Вставляет несколько строк в одном запросе
В настоящее время я использую этот тип SQL на MySQL, чтобы вставить несколько строк значений в одном запросе:
INSERT INTO `tbl` (`key1`,`key2`) VALUES ('r1v1','r1v2'),('r2v1','r2v2'),...
В чтениях по PDO операторы, подготовленные к использованию, должны обеспечить мне большую безопасность, чем статические запросы.
Поэтому я хотел бы знать, возможно ли сгенерировать "вставку нескольких строк значений с помощью одного запроса" с использованием подготовленных операторов.
Если да, могу ли я узнать, как я могу это реализовать?
27 ответов
Вставка нескольких значений с подготовленными заявлениями 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 секунду при использовании нескольких вставок и обычных подготовленных вставок с одним значением.
Тот же ответ, что и г-н Балагтас, немного яснее...
Последние версии MySQL и PHP PDO поддерживают несколько строк INSERT
заявления.
Обзор SQL
SQL будет выглядеть примерно так, если предположить, что вы хотите 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)
ЗОП звонит.
$params
будет одномерный массив всех значений для передачи INSERT
,
В приведенном выше примере он должен содержать 9 элементов; PDO будет использовать каждый набор из 3 как один ряд значений. (Вставка 3 строк по 3 столбца в каждом = массив из 9 элементов.)
Реализация
Ниже код написан для ясности, а не эффективности. Работа с PHP array_*()
функции для улучшения способов отображения или просмотра ваших данных, если хотите. Можно ли использовать транзакции, очевидно, зависит от типа таблицы 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();
Что бы это ни стоило, я видел, что многие пользователи рекомендуют выполнять итерации по операторам 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 раз быстрее или больше. Если бы это было сделано, чтобы отразить импорт тысяч строк во многих других столбцах, разница могла бы быть огромной.
Принятый ответ Герберта Балагтаса хорошо работает, когда массив $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();
Два возможных подхода:
$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();
}
Если данные для всех строк находятся в одном массиве, я бы использовал второе решение.
Это просто не то, как вы используете готовые заявления.
Вполне нормально вставлять одну строку на запрос, потому что вы можете выполнить один подготовленный оператор несколько раз с разными параметрами. Фактически, это одно из величайших преимуществ, поскольку оно позволяет вам эффективно, безопасно и удобно вставлять большое количество строк.
Так что, возможно, можно реализовать предложенную вами схему, по крайней мере, для фиксированного числа строк, но почти гарантировано, что это не совсем то, что вам нужно.
Более короткий ответ: сгладьте массив данных, упорядоченный по столбцам, затем
//$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 записей или около того вам не нужно проходить через каждую запись, чтобы вставить их, когда все, что вам нужно - это подсчет значений.
Вот мой простой подход.
$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();
Вот класс, который я написал, чтобы сделать несколько вставок с опцией очистки:
<?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;
}
}
Вот еще одно (тонкое) решение этой проблемы:
Сначала вам нужно подсчитать данные исходного массива (здесь: $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();
Основываясь на своих экспериментах, я обнаружил, что инструкция вставки mysql с несколькими строками значений в одной транзакции является самой быстрой.
Однако, если данных слишком много, то MySQL max_allowed_packet
установка может ограничить вставку одной транзакции несколькими строками значений. Следовательно, следующие функции потерпят неудачу, когда есть данные больше, чем MySQL max_allowed_packet
размер:
singleTransactionInsertWithRollback
singleTransactionInsertWithPlaceholders
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();
Результаты для 100000 записей для таблицы, содержащей только два столбца, приведены ниже
$ 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
Вот как я это сделал:
Сначала определите имена столбцов, которые вы будете использовать, или оставьте это поле пустым, и pdo будет предполагать, что вы хотите использовать все столбцы таблицы - в этом случае вам нужно будет сообщить значения строк в точном порядке, в котором они отображаются в таблице.,
$cols = 'name', 'middleName', 'eMail';
$table = 'people';
Теперь предположим, что у вас уже есть двухмерный массив. Итерируйте его и создайте строку с вашими значениями строки, как таковые:
foreach ( $people as $person ) {
if(! $rowVals ) {
$rows = '(' . "'$name'" . ',' . "'$middleName'" . ',' . "'$eMail'" . ')';
} else { $rowVals = '(' . "'$name'" . ',' . "'$middleName'" . ',' . "'$eMail'" . ')';
}
Теперь, что вы только что сделали, это проверили, было ли уже определено $rows, и если нет, создайте его и сохраните значения строк и необходимый синтаксис SQL, чтобы он был действительным оператором. Обратите внимание, что строки должны заключаться в двойные и одинарные кавычки, поэтому они будут быстро распознаваться как таковые.
Все, что осталось сделать, это подготовить оператор и выполнить так:
$stmt = $db->prepare ( "INSERT INTO $table $cols VALUES $rowVals" );
$stmt->execute ();
До сих пор тестировалось до 2000 строк, и время выполнения является мрачным. Проведу еще несколько тестов и вернусь сюда, если у меня будет что-то еще.
С уважением.
Поскольку это еще не было предложено, я уверен, что LOAD DATA INFILE по-прежнему является самым быстрым способом загрузки данных, поскольку он отключает индексирование, вставляет все данные, а затем повторно включает индексы - все в одном запросе.
Сохранение данных в формате csv должно быть довольно тривиальным, учитывая fputcsv. MyISAM работает быстрее, но вы все равно получаете большую производительность в InnoDB. Есть и другие недостатки, поэтому я бы пошел по этому пути, если вы вставляете много данных, и не беспокоитесь о менее чем 100 строках.
Вот мое решение: 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);
Сообщения об ошибках приветствуются.
Как видно из ответов, предшествующих моим, делать это с ванилью затруднительно.PDO
. Уровни абстракции базы данных, такие как Nette, значительно облегчают задачу без существенного влияния на скорость:
$params = [
'host' => 'sandbox-db',
'database' => 'test',
'user' => 'root',
'pass' => '123',
];
$database = new Nette\Database\Connection("mysql:host={$params['host']};dbname={$params['database']};charset=utf8", $params['user'], $params['pass']);
$staff = [
[
'birthday' => new DateTime('1995-05-01'),
'name' => 'Sharon',
'salary' => '200',
'boss' => true,
],
[
'birthday' => new DateTime('2000-01-01'),
'name' => 'John',
'salary' => '140',
'boss' => false,
],
[
'birthday' => new DateTime('1985-08-01'),
'name' => 'Oliver',
'salary' => '120',
'boss' => false,
],
];
$database->beginTransaction();
$database->query('INSERT INTO test', $staff);
$database->commit();
Хотя старый вопрос, все вклады мне очень помогли, так что вот мое решение, которое работает в моем собственном 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();
}
Возможно использование одного подготовленного оператора и одного параметра привязки для нескольких строк, изменяя только аргументы, предоставленные оператору. Мой проект показал лучшие результаты при использовании этого метода (0,398 с), чем при построении длинного оператора с длинным параметром привязки (2,508 с).
Важная заметка:
- Я использовал InnoDB для своей таблицы, использование транзакции важно для повышения производительности.
- С таблицей MyISAM вы можете получить выгоду, используя ЗАГРУЗКУ ДАННЫХ из файла https://dev.mysql.com/doc/refman/8.0/en/optimizing-myisam-bulk-data-loading.html.
На примере @jamesvi я использовал этот метод для своего проекта.
$dataVals = [['a', 'b', 'c'], ['d', 'e', 'f']];
$colAIn = '';
$colBIn = '';
$colCIn = '';
// Depending on the keys you have on your table, disabling some more checks might increase performance
// mysqli_query($con, "SET AUTOCOMMIT = 0;"); // this should be unecessary since we're using transactions
// mysqli_query($con, "SET FOREIGN_KEY_CHECKS = 0;");
// mysqli_query($con, "SET UNIQUE_CHECKS = 0;");
mysqli_begin_transaction($con);
$stmt = mysqli_prepare($con, "INSERT INTO tblName (colA, colB, colC) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "sss", $colAIn, $colBIn, $colCIn);
for ($i = 0; $i < count($dataVals); $i++) {
$colAIn = $dataVals[$i][0];
$colBIn = $dataVals[$i][1];
$colCIn = $dataVals[$i][2];
mysqli_stmt_execute($stmt);
}
mysqli_commit($con);
// mysqli_query($con, "SET AUTOCOMMIT = 0;"); // this should be unecessary since we're using transactions
// mysqli_query($con, "SET FOREIGN_KEY_CHECKS = 0;");
// mysqli_query($con, "SET UNIQUE_CHECKS = 0;");
Извините за процедурный стиль, виню в этом предшественников моего проекта.
Для людей, у которых могут возникнуть проблемы с пониманием длинных рядов кодов:
Обязательно используйте один массив для каждой вставляемой строки. Например:
$data = [ ['name'=>'Sam', 'job'=>'fullstack', 'device'=>'hp'], ['name'=>'Joey', 'job'=>'ui', 'device'=>'apple'] ] //the subarrays in $data can also be indexed arrays
Идея состоит в том, чтобы иметь возможность легко формировать запрос SQL Insert для нескольких строк, который должен выглядеть следующим образом:
insert into table_name (col1, col2, col3) values ('valA1', 'valA2', 'valA3'), ('valB1', 'valB2', 'valB3'); //normal sql, insecure
insert into table_name (col1, col2, col3) values (?, ?, ?), (?, ?, ?); //prepared statement, secure
Надеюсь, я все еще в здравом уме. Если вы будете выполнять эту пакетную вставку с динамическими именами столбцов, вам всегда необходимо иметь массив ожидаемых имен столбцов и присваивать им значения по умолчанию. Например:
$expectedcols = ['name'=>'', 'job'=>'', 'device'=>''];
Затем;
$validcols = array_intersect_key($data[0], $expectedcols); //Takes only valid column names and throws away unexpected column names
$allvalidcols = array_merge($expectedcols, $validcols); //assigns the values in $validcols to the values in $expectedcols. Other columns in $expectedcols required to be inserted will also be captured, but their values will be those default values you assigned to them earlier.
$col_arr = array_keys($allvalidcols); //extracts the safe column names.
$columns = implode(', ', $col_arr); //name, job, device
$cols = count($col_arr); //number of columns, 3
$temparr = array_fill(0, $cols, '?'); //first set of placeholders ['?', '?', '?']
$tempstr = '('.implode(', ', $temparr).')'; //(?, ?, ?)
$rows = count($data); //number of rows, 2
$totalarr = array_fill(0, $rows, $tempstr); //full set of placeholders ['(?, ?, ?)', '(?, ?, ?)']
$totalstr = implode(', ', $totalarr); //(?, ?, ?), (?, ?, ?)
$allarr = [];
foreach($data as $ind=>$val) {
$values = array_values($val);
$allarr = array_merge($allarr, $values); //['Sam', 'fullstack', 'hp', 'Joey', 'ui', 'apple']
}
$sql = "insert into table_name ($columns) values $totalstr";
Но если вы не выполняете пакетную вставку с динамическими именами столбцов, пропустите $validcols, $allvalidcols, $col_arr и $columns, они не очень нужны, а затем установите $cols на количество столбцов, которое в этом примере равно 3. , затем измените запрос $sql следующим образом:
$sql = "insert into table_name (name, age, device) values $totalstr";
И наконец;
$stmt = $conn->prepare($sql);
$done = $stmt->execute($allarr);
Мой пример из реального мира, чтобы вставить все немецкие почтовые индексы в пустую таблицу (чтобы добавить названия городов позже):
// 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).
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);
}
}
?>
Это сработало для меня
$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();
Вы можете вставить несколько строк в один запрос с помощью этой функции:
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')));
Преимущество заключается в том, что вы используете подготовленные операторы, вставляя несколько строк одним запросом. Безопасность!
У меня была та же проблема, и вот как я выполняю для себя, и я сделал для себя функцию для нее (и вы можете использовать ее, если это поможет вам).
Пример:
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, ваши данные были вставлены в вашу базу данных.
Большинство решений, приведенных здесь для создания подготовленного запроса, являются более сложными, чем они должны быть. Используя встроенные функции 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);
}
Вот чистое решение для вставки нескольких строк с помощью PDO.
Он должен работать с php 7.1+ из-за деструктуризации массива, но я думаю, эту часть можно легко изменить.
function insertMultipleQuery(string $table, array $columns, array $items): array
{
$placeholders = '';
foreach ($items as $item) {
if (!empty($placeholders)) {
$placeholders .= ', ';
}
$placeholders .= '(' . implode(',', array_fill(0, count($item), '?')) . ')';
}
$names = '`' . implode("`,`", $columns) . '`';
$query = "INSERT INTO {$table} ({$names}) VALUES {$placeholders}";
$values = [];
foreach ($items as $item) {
foreach ($item as $value) {
$values[] = $value;
}
}
return [
'query' => $query,
'values' => $values,
];
}
Образец данных:
// $pdo = new PDO(...........); // Fill in your connection config
$tableName = 'table_name';
// It should be obvious - the keys of the array is actual column names in your table in database.
$columns = [
'name',
'value',
'created',
];
$dataToInsert = [
[
'name' => 'some name 1',
'value' => 'some value 1',
'created' => 'created datetime 1',
],
[
'name' => 'some name 2',
'value' => 'some value 2',
'created' => 'created datetime 2',
]
];
[
'query' => $query,
'values' => $values,
] = $this->insertMultipleQuery($tableName, $columns, $dataToInsert);
$stmt = $pdo->prepare($query);
$stmt->execute($values);
Как насчет этого:
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);
}
Идея заключается в том, чтобы циклически перебирать значения вашего массива, добавляя "номера идентификаторов" в каждый цикл для ваших подготовленных заполнителей операторов, в то же время вы добавляете значения в свой массив для параметров привязки. Если вам не нравится использовать "ключевой" индекс из массива, вы можете добавить внутри цикла $i=0 и $i++. Любой из них работает в этом примере, даже если у вас есть ассоциативные массивы с именованными ключами, он все равно будет работать, если ключи будут уникальными. Немного поработав, это подойдет и для вложенных массивов.
** Обратите внимание, что substr удаляет последние пробелы и запятую в переменных $sql, если у вас нет пробела, вам нужно изменить это значение на -1, а не на -2.
Массив union должен быть еще быстрее чем array_push
так что-то вроде:
$cumulativeArray += $rowArray;