PHP e PDO: inserire e aggiornare più record in modo efficiente

by theArchitect
SHARE
PHP e PDO: inserire e aggiornare più record in modo efficiente
© Guida-HTML5.it

Introduzione

Quando un’applicazione deve salvare molti dati, eseguire una query separata per ogni record può diventare lento e difficile da mantenere. Un esempio comune è l’importazione di prodotti da un file, la registrazione di più partecipanti a un evento oppure l’aggiornamento di diverse righe dopo un’operazione amministrativa.

PDO offre gli strumenti necessari per gestire queste situazioni in modo sicuro ed efficiente. L’approccio consigliato consiste nel preparare una query una sola volta, riutilizzarla per ogni elemento e racchiudere l’operazione in una transazione. In questo modo si riduce il lavoro del database e si garantisce che tutti i dati vengano salvati correttamente oppure che nessuna modifica venga applicata in caso di errore.

Nel tutorial useremo una tabella products con le colonne name, price e stock. L’esempio mostra anche come validare i dati prima dell’inserimento e come distinguere correttamente i tipi dei parametri.

Codice completo

Supponiamo di avere già creato la seguente tabella MySQL:

CREATE TABLE products (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(120) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT UNSIGNED NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Il seguente script inserisce più prodotti usando una sola query preparata e una transazione:

<?php

declare(strict_types=1);

$dsn = ´mysql:host=localhost;dbname=shop;charset=utf8mb4´;
$username = ´app_user´;
$password = ´password_sicura´;

$products = [
    [
        ´name´ => ´Tastiera meccanica´,
        ´price´ => 89.90,
        ´stock´ => 15,
    ],
    [
        ´name´ => ´Mouse wireless´,
        ´price´ => 34.50,
        ´stock´ => 28,
    ],
    [
        ´name´ => ´Cuffie USB´,
        ´price´ => 49.99,
        ´stock´ => 10,
    ],
];

try {
    $pdo = new PDO($dsn, $username, $password, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    // Controlla i dati prima di iniziare la transazione.
    foreach ($products as $product) {
        if (
            !isset($product[´name´], $product[´price´], $product[´stock´]) ||
            trim($product[´name´]) === ´´ ||
            !is_numeric($product[´price´]) ||
            !filter_var($product[´stock´], FILTER_VALIDATE_INT) === false
        ) {
            throw new InvalidArgumentException(
                ´Uno o più prodotti contengono dati non validi.´
            );
        }

        if ((float) $product[´price´] < 0 || (int) $product[´stock´] < 0) {
            throw new InvalidArgumentException(
                ´Prezzo e disponibilità non possono essere negativi.´
            );
        }
    }

    $pdo->beginTransaction();

    $sql = ´
        INSERT INTO products (name, price, stock)
        VALUES (:name, :price, :stock)
    ´;

    // La query viene preparata una sola volta.
    $statement = $pdo->prepare($sql);

    foreach ($products as $product) {
        $name = trim($product[´name´]);
        $price = (string) $product[´price´];
        $stock = (int) $product[´stock´];

        $statement->bindValue(´:name´, $name, PDO::PARAM_STR);
        $statement->bindValue(´:price´, $price, PDO::PARAM_STR);
        $statement->bindValue(´:stock´, $stock, PDO::PARAM_INT);

        $statement->execute();
    }

    $pdo->commit();

    echo ´Prodotti inseriti correttamente.´;

} catch (Throwable $exception) {
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    error_log($exception->getMessage());
    echo ´Impossibile completare l’importazione dei prodotti.´;
}

Spiegazione

Connessione configurata correttamente

L’opzione PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION consente a PDO di generare eccezioni quando si verifica un errore. Questo rende il flusso più leggibile rispetto al controllo manuale del valore restituito da ogni metodo.

È inoltre consigliabile disabilitare l’emulazione dei prepared statements con PDO::ATTR_EMULATE_PREPARES => false. In questo modo, quando il driver lo supporta, la query viene preparata realmente dal database.

Preparare una sola volta

La query viene preparata prima del ciclo:

$statement = $pdo->prepare($sql);

Successivamente vengono cambiati soltanto i valori dei parametri. Preparare la stessa query a ogni iterazione aggiungerebbe lavoro inutile e renderebbe il codice meno efficiente.

Tipi dei parametri

Il metodo bindValue() permette di specificare il tipo del valore:

  • PDO::PARAM_STR per stringhe;
  • PDO::PARAM_INT per numeri interi;
  • PDO::PARAM_BOOL per valori booleani;
  • PDO::PARAM_NULL per valori nulli.

Per il prezzo viene usato PDO::PARAM_STR. Anche se rappresenta un numero, il tipo SQL DECIMAL è più sicuro da gestire come stringa, evitando problemi di precisione tipici dei numeri floating point.

Perché usare una transazione

La transazione inizia con beginTransaction() e termina con commit(). Se un prodotto non può essere inserito, il blocco catch esegue rollBack().

Senza transazione, i primi prodotti potrebbero essere salvati mentre quelli successivi falliscono. Il database si troverebbe quindi in uno stato parzialmente aggiornato. Con il rollback, invece, l’operazione viene trattata come un’unica unità logica.

Best practice

  • Valida sempre i dati prima di salvarli: controlla stringhe vuote, numeri negativi, lunghezze massime e valori obbligatori.
  • Non concatenare valori nella query: usa sempre placeholder e prepared statements.
  • Riutilizza lo statement: prepara la query una volta sola quando la struttura SQL non cambia.
  • Usa transazioni per operazioni correlate: sono particolarmente importanti durante importazioni e aggiornamenti multipli.
  • Non mostrare dettagli tecnici all’utente: registra l’errore nei log e mostra un messaggio generico.
  • Imposta vincoli anche nel database: colonne NOT NULL, chiavi uniche e controlli SQL rappresentano una seconda linea di difesa.
  • Gestisci grandi quantità di dati a blocchi: migliaia di righe in un’unica transazione possono consumare molta memoria o mantenere i lock troppo a lungo.
  • Usa nomi di connessione separati: le credenziali del database non dovrebbero essere scritte direttamente in file pubblici o distribuiti.

Se il numero di record è molto elevato, può essere preferibile dividere l’importazione in gruppi, ad esempio da 500 o 1.000 elementi. Ogni gruppo può avere una propria transazione, riducendo il rischio di transazioni troppo lunghe.

Riepilogo

Per inserire o aggiornare molti record con PHP e PDO, è utile combinare tre tecniche: prepared statements, riutilizzo della query e transazioni. Il prepared statement protegge dai dati inseriti direttamente nella query e migliora la gestione dei parametri. Riutilizzare lo statement evita preparazioni ripetute. La transazione garantisce invece coerenza: tutte le operazioni vengono confermate insieme oppure annullate in caso di errore.

Questo schema è adatto a importazioni, sincronizzazioni, caricamenti massivi e processi amministrativi. Prima di applicarlo in produzione, aggiungi validazioni coerenti con il dominio dell’applicazione e verifica i limiti del database utilizzato.

Approfondisci con risorse ufficiali

SHARE