Підготовлений PDO Вставляє кілька рядків в один запит


145

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

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

Під час читання PDO заявка про використання повинна забезпечити менший захист, ніж статичні запити.

Тому я хотів би знати, чи можна генерувати "вставлення декількох рядків значень за допомогою одного запиту", використовуючи підготовлені оператори.

Якщо так, чи можу я знати, як я можу це здійснити?


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

Відповіді:


150

Вставити декілька значень з готовими заявами PDO

Вставлення декількох значень в один оператор Execute. Чому тому, що відповідно до цієї сторінки це швидше, ніж звичайні вставки.

$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)." ", ЗАРАЗ ()) ';
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 - ім'я рядка таблиці в INSERT в
  • $colNames- одновимірний масив імен стовпців таблиці Ці імена стовпців повинні бути дійсними ідентифікаторами стовпців MySQL; уникайте їх за допомогою зворотних посилань (``), якщо їх немає
  • $dataVals - багатовимірний масив, де кожен елемент - це 1-d масив ряду значень для 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();

6
Це дуже погано, що PDO обробляє це таким чином, є деякі дуже елегантні способи зробити це в інших драйверах БД.
Джонатан

Це налаштовує заповнювачі ще більш жорстко, $rowPlacesне роблячи більше необхідних:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil

Працює ідеально. Я би додав до цієї відповіді необхідність забезпечення унікальності (комбінації) індексів у таблиці. Як і в інструкції ALTER TABLE votesADD UNIQUE unique_index( user, email, address);
Джузеппе

1
Дивовижно! До речі, використання array_push($dataToInsert, ...array_values($dataVals));буде набагато швидшеforeach ($dataVals as $row => $data) {}
Аніс

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 - відмінна ідея розмістити 10 рядків безпосередньо в одному виконанні . Але як я можу вставити тисячі рядків, коли вони зберігаються в такому об'єкті, як JSON? Мій код нижче працює ідеально. Але як я можу налаштувати його, щоб вставити 10 рядків за одне виконання? `foreach ($ json_content як $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [дата]; $ row3 = $ datarow [рядок3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [рядок6]; $ row7 = $ datarow [рядок7]; // тепер виконати $ databaseinsert-> Execute (); } // кінець передвісті `
Петро,

@ JM4 - ... і моє друге запитання: "чому bind_paramу другому режимі імпорту немає заяви"?
Пітер

Хіба вам не доведеться двічі крутити? Вам також доведеться динамічно генерувати (?,?), правда?
NoobishPro

@NoobishPro Так, ви можете використовувати те саме для / foreach, щоб генерувати обидва.
Chazy Chaz

34

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

Використання array_push () для створення $ insert_values ​​замість array_merge () призвело до прискорення 100X із часом виконання 0,41s .

Проблемний масив_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));. Набагато швидше.
квітня

Чому 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();
}

Якщо дані для всіх рядків знаходяться в одному масиві, я використовував би друге рішення.


10
в останньому, чи не ви тоді робите кілька (можливо, тисяч) окремих виконувати дзвінки, а не поєднувати в одне твердження?
JM4

@ JM4, ви припускаєте, що вона $stmt->execute();повинна знаходитися поза циклом foreach?
bafromca

@bafromca - Так, є. Дивіться мою відповідь вище з обґрунтуваннями. У чистому операторі вставки немає ніяких причин, я можу логічно придумати, що це не може бути одним твердженням. Один дзвінок, один виконання. Насправді мою відповідь з початку 2012 року можна було б покращити ще більше - те, що я зроблю пізніше, коли матиму ще трохи часу. Якщо ви почнете вводити комбінації Вставити / оновити / видалити, це вже інша історія.
JM4

12

Це просто не спосіб використання готових висловлювань.

Цілком нормально вставляти один рядок за запитом, оскільки ви можете виконати одне підготовлене твердження кілька разів з різними параметрами. Насправді це одна з найбільших переваг, оскільки дозволяє ефективно вставляти вам велику кількість рядків у ефективний, безпечний та зручний спосіб.

Тож можливо можливо реалізувати запропоновану вами схему хоча б для фіксованої кількості рядків, але майже гарантовано, що це насправді не те, що ви хочете.


1
Чи можете ви запропонувати кращий спосіб вставити кілька рядків у таблицю?
Crashthatch

@Crashthatch: Просто зробіть це наївним способом: Установіть підготовлену операцію один раз, а потім виконайте її для кожного рядка з різними значеннями для прив’язаних параметрів. Це другий підхід у відповіді Зіка.
sebasgo

2
Мета, яку ви згадали для підготовленої заяви, є правильною. Але використання multi -insert - це ще одна методика для покращення швидкості вставки, і її можна використовувати і з підготовленим оператором. На моєму досвіді, переносячи 30 мільйонів даних про рядки за допомогою PDO, підготовленого оператора, я побачив, що мульти-вставка була в 7-10 разів швидшою, ніж згрупована одна вставка в транзакціях.
Аніс

1
Абсолютно згоден з Аніс. Я маю 100k рядків і отримую величезне збільшення швидкості за допомогою вставки рядків muli.
Кеннет

Стверджувати, що викликати реляційну базу даних у циклі один раз у ряд, як правило, це добре, з чим я не можу погодитися. З цього питання. Зрозуміло, іноді це нормально. Я не вірю в абсолюти з технікою. Але це антидіаграма, яку слід використовувати лише в окремих випадках.
Брендон

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 або більше, вам не потрібно переглядати кожен запис, щоб вставити їх, коли все, що вам потрібно, - це підрахунок значень.


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 і також включив деякі ідеї звідси. Я просто не бачив твого класу. Вибачте, будь ласка, тут про мою безсоромну саморекламу, але я думаю, це комусь допоможе. Сподіваємось, це не проти 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'" . ')';
}

Тепер, що ви тільки що зробили, це перевірити, чи $ рядки вже визначені, а якщо ні, створіть їх і збережіть значення рядків та необхідний синтаксис SQL, щоб він був дійсним твердженням. Зауважте, що рядки повинні містити подвійні лапки та одинарні лапки, тому вони будуть негайно визнані такими.

Залишилося лише підготувати заяву та виконати як таке:

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

Протестовано до 2000 рядків до цього часу, а час виконання є похмурим. Пройду ще кілька тестів і повернусь сюди, якщо у мене є щось додаткове.

З повагою


1

Оскільки це ще не запропоновано, я впевнений, що ЗАВАНТАЖЕННЯ ДАНИХ INFILE все-таки є найшвидшим способом завантаження даних, оскільки він відключає індексацію, вставляє всі дані, а потім знову вмикає індекси - все в одному запиті.

Збереження даних у форматі csv має бути досить тривіальним, маючи на увазі fputcsv. MyISAM найшвидший, але ви все одно отримуєте великі показники в InnoDB. Є й інші недоліки, хоча тому я би пішов цим маршрутом, якщо ви вставляєте багато даних, а не турбуєтесь із меншими за 100 рядків.


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).


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

}
?>

Ласкаво просимо в stackoverflow. Не лише код, будь ласка, опублікуйте свою проблему та поясніть.
Prakash Palnati

в основному. це лише реалізація коду, який міститься у прийнятій відповіді
Ваше здорове чуття

0

У мене була така ж проблема, і саме це я досягаю для себе, і я створив для себе функцію (і ви можете використовувати її, якщо це допоможе вам).

Приклад:

ВСТАВЛЯЄТЬСЯ в країни (країна, місто) ЦІННОСТІ (Німеччина, Берлін), (Франція, Париж);

$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 , ваші дані були вставлені у вашу базу даних.


0

На основі своїх експериментів я з'ясував, що оператор mysql insert з декількома рядками значення в одній транзакції є найшвидшим.

Однак якщо даних занадто багато, то параметр mysql max_allowed_packetможе обмежувати вкладку однієї транзакції кількома рядами значень. Отже, наступні функції не зможуть, коли є дані, що перевищують 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

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

Ідея цього полягає в тому, щоб переглядати значення масиву, додаючи "ідентифікаційні номери" до кожного циклу для підготовлених заповнювачів виписки, в той же час ви додаєте значення до масиву для параметрів прив'язки. Якщо вам не подобається використовувати індекс "ключ" з масиву, ви можете додати $ 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);
}

1
Цей код ніколи не слід використовувати, оскільки він вразливий до ін'єкції SQL
Ваше звичайне чуття
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.