Обновление нескольких записей одним запросом в MySQL

Начну сразу с абстрактного примера. Представьте такую ситуацию: у вас интернет магазин и одним из функционалов в админке, вы сделали возможность изменить вручную цены сразу нескольким товарам. Мы рассматриваем именно ручное изменение, а не по какой-нибудь формуле, например, увеличить цену на N процентов. Как бы вы реализовали это обновление? Как обновляли бы поля цен для каждого определенного товара? Первое, что пришло бы на ум новичку (да и не только) - это запросы в цикле. И в таком случае, количество запросов к базе было бы равным количеству обновляемых товаров. Ну, иногда и без такой конструкции не обойтись. Но если ситуация позволяет, то обновить несколько записей можно одним запросом. И сейчас, я покажу, как это можно сделать. Продолжим эксперимент на вышеуказанном примере. Выводим из БД нужные данные, где нам необходимы названия товаров, их текущие цены и их ID. Эти все значения распределяем в форме:

<form action="handler.php"  method="post" name="multiple_update">
  <p>
    <label>Apple iPhone 7:</label><!-- название товара -->
    <input type="hidden" name="item_id[]" value="7"><!-- ID товара в базе -->
    <input type="text" name="item_price[]" value="250"><!-- текущая цена товара -->
  </p>
  <p>
    <label>Samsung Galaxy S10:</label>
    <input type="hidden" name="item_id[]" value="3">
    <input type="text" name="item_price[]" value="300">
  </p>
  <p>
    <label>Nokia Lumia 1000:</label>
    <input type="hidden" name="item_id[]" value="5">
    <input type="text" name="item_price[]" value="150">
  </p>
  <p>
    <input type="submit" value="Сохранить">
  </p>
</form>

Хочу обратить внимание на одинаковые названия и квадратные скобки в именах полей! Вы, конечно же, знаете, но для тех, кто не в курсе, поясню: квадратные скобки указывают, что значения передаются массивом, а ключами будут являться значения до скобок. То есть, отправив форму и выведя результат на экран, мы получим следующую картину:

Array (
  [item_id] => Array (
    [0] => 7
    [1] => 3
    [2] => 5
  )
  [item_price] => Array (
    [0] => 250
    [1] => 300
    [2] => 150
  )
)

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

if(isset($_POST['item_id'], $_POST['item_price'])){
  $item_id = $_POST['item_id'];
  $item_price = $_POST['item_price'];
  foreach($_POST['item_id'] as $k => $v){ // (см. ниже¹)
    // Выполнять запросы не будем, а просто выведем их для наглядности
    echo "UPDATE `products` SET `price` = ".$item_price[$k]." WHERE `prod_id` = ".$v;
  }
}

// (прим.¹)кстати, если у вас только два значения, то цикл можно организовать таким образом,
foreach(array_combine($_POST['item_id'],$_POST['item_price']) as $val_id => $val_price){
  echo "UPDATE `products` SET `price` = ".$val_price." WHERE `prod_id` = ".$val_id;
}
// если же, допустим, вы будете обновлять еще и названия товаров, то такой способ не подойдёт

Смысл, я думаю, что понятен. Обновляем три товара - выполняем три отдельных запроса. Ладно, если три, но, а если пару десятков? Как-то не комильфо ;) Так вот, для того, чтоб построить для такой операции один единственный запрос, нам поможет, как бы это странно не выглядело, оператор INSERT. В MySQL для этого оператора предусмотрен синтаксис: "INSERT ... ON DUPLICATE KEY UPDATE ..." Если описать его простым языком, то получится что-то вроде: "Добавить новую запись в таблицу, но если запись с таким ключом (уникальным или первичным) существует, то просто обновить в ней данные." Особенность с ключами нужно учитывать, иначе фокус не удастся... То есть, в запросе должно фигурировать одно или несколько полей с уникальным (UNIQUE) или первичным ключом (PRIMARY KEY). Тогда при выполнении запроса и совпадении значений в таких полях, вставка новой записи будет проигнорирована, а записи остальных полей обновлены данными. В нашем случае, поле "prod_id" - это ключ первичный, поэтому без опасений перейдём к генерированию запроса.

if(isset($_POST['item_id'], $_POST['item_price'])){
  $item_id = $_POST['item_id'];
  $item_price = $_POST['item_price'];
  // Объединяем массивы
  $allData = array_map( null, $item_id, $item_price );
  // Генерируем запрос
  $query = "INSERT INTO `products` (`prod_id`,`price`) VALUES ";
  $params = array();
  foreach($allData as $val){
    $params[] = '('.(int)$val[0] .','.(float)$val[1].')';
  }
  $query .= implode(',', $params) . " ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)";
  // Запрос готов.
  echo $query; // можно вывести его на экран для проверки
  // Остается его выполнить
}

Тут важно следить за порядком! Если при объединении массивов данных, мы ставим первым (после "null") ID товара, то учитываем, что в цикле $val[0] - это тоже айдишник товара и в перечислении полей - "INSERT INTO `products` (`prod_id`,`price`)" - первое должно быть соответствующее. И так далее для остальных данных. По поводу синтаксиса функции array_map() с использование первого параметра "null", дабы не раздувать тему, предлагаю почитать в документации. Данный пример уже готов к использованию, если вы всё ещё пользуетесь устаревшим расширением mysql_*, но если вы уже счастливчик, который перешёл на PDO или MySQLi, то процесс подготовки данных и запроса, нужно адаптировать. Тут всё зависит от ваших личных предпочтений и особенностей работы с БД, но в качестве примера, покажу, как это можно сделать для PDO и запроса с неименованными плейсхолдерами.

if(isset($_POST['item_id'], $_POST['item_price'])){
  try {
    require 'db.php'; // Подключение к БД
    
    $item_id = $_POST['item_id'];
    $item_price = $_POST['item_price'];
    
    // Объединяем массивы данных
    $allData = array_map( null, $item_id, $item_price );
    
    // Подготавливаем строку запроса
    $query = "INSERT INTO `products` (`prod_id`,`price`) VALUES ";
    $query .= substr(str_repeat('(?, ?),', count($allData)), 0, -1);
    $query .= " ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)";
    //echo $query; // можно вывести строку запроса для контроля
    
    $dbh = DB::getInstance();
    $sth = $dbh->prepare($query);
    
    // создаём одномерный массив всех значений из двумерного (см. ниже²)
    $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($allData));
    $allData = iterator_to_array($iterator, false);
    
    // Привязываем параметры запроса к переменным
    foreach($allData as $key => &$val){
      $sth->bindParam(($key+1), $val);
    }
    
    // выполняем запрос
    $sth->execute();
    echo ($sth->rowCount() > 0 ? 'Данные успешно обновлены!' : 'Не произведено ни одной замены'),
      '<br><a href="./">Вернутся</a> на страницу редактирования';
  } catch(PDOException $e){
    echo 'Ошибка при выполнении запроса: ', $e->getMessage();
  }
}

(прим.²) Способ преобразования многомерных массивов в одномерные, описан в этой статье. Данный способ не ограничивает нас обновлением одного поля. Таких полей, можно добавлять сколько угодно, но и как в предыдущих вариантах, необходимо следить за порядком подстановки данных, плейсхолдеров и т.д. update_table.rar

02.03.2023

578
A B i U S JS

PHP HTML CSS
Чат
    Для входа только имэйл или имя и апроль
    Можно сменить аватар
    Имэйл Ваше имя
    Пароль