Vai al contenuto

CRUD: inserire, modificare e cancellare dal sito

In questa lezione impari a

  • organizzare l'applicazione in file con un kit comune
  • mostrare la scheda di un record a partire dall'id
  • usare un unico form per inserire e modificare
  • cancellare in modo sicuro con POST e gestire i vincoli delle chiavi esterne
  • mostrare un messaggio dopo il redirect

CRUD è la sigla delle quattro operazioni che ogni applicazione gestionale fa sui suoi dati: Create (inserire), Read (leggere), Update (modificare), Delete (cancellare). In SQL sono INSERT, SELECT, UPDATE e DELETE; in questa lezione le portiamo nel sito, per la tabella dei libri.

Il kit della biblioteca

Per non perdere tempo con la grafica, parti dal kit: contiene la configurazione, la connessione, le funzioni comuni, l'intestazione con il menu, il piè di pagina e un foglio di stile che funziona anche sul telefono.

Estrai lo zip in C:\xampp\htdocs, in modo da avere C:\xampp\htdocs\biblioteca\config.php. Se hai già una cartella biblioteca con gli esercizi delle lezioni 10 e 11, rinominala prima (per esempio biblioteca-prove).

File Contenuto
config.php dati del database e due impostazioni: libri per pagina, giorni di prestito
db.php la connessione $conn della lezione 10
funzioni.php h() contro l'XSS, messaggio() e vai() per i redirect, e le funzioni di login e sicurezza che useremo nella lezione 13
header.php, footer.php l'inizio e la fine di ogni pagina, con il menu
stile.css la grafica

Ogni pagina dell'applicazione ha sempre questa forma:

<?php
require 'funzioni.php';        // 1. funzioni e connessione

// 2. la logica: legge i parametri, esegue le query, se serve fa un redirect

$titolo = 'Titolo della pagina';
require 'header.php';          // 3. da qui in giù solo HTML
?>
<h1>...</h1>
<?php require 'footer.php'; ?>

Tutta la logica sta prima di header.php: così i redirect con header('Location: ...') funzionano sempre (lezione 9).

Tre funzioni da conoscere

Apri funzioni.php. Oltre a h(), che conosci, ci sono:

<?php
// Messaggio da mostrare nella pagina successiva (dopo un redirect)
function messaggio(string $testo, string $tipo = 'ok'): void
{
    $_SESSION['messaggio'] = ['testo' => $testo, 'tipo' => $tipo];
}

function vai(string $pagina): void
{
    header('Location: ' . $pagina);
    exit;
}
  • vai('index.php') fa il redirect e ferma lo script: è lo schema Post/Redirect/Get in una riga.
  • messaggio('Libro salvato.') "ricorda" un messaggio, che header.php mostra nella pagina successiva e poi cancella. Per ricordarlo tra una pagina e l'altra usa la sessione ($_SESSION), una piccola memoria che il server tiene per ogni visitatore: la studiamo per bene nella prossima lezione. Per ora ti basta sapere che il messaggio sopravvive al redirect.

R: la scheda del libro

La pagina libro.php?id=7 mostra un libro e lo storico dei suoi prestiti.

<?php
require 'funzioni.php';

$id = (int) ($_GET['id'] ?? 0);
$stmt = mysqli_prepare($conn, 'SELECT l.*, a.nome, a.cognome, a.nazionalita
                               FROM libri l JOIN autori a ON a.id = l.autore_id
                               WHERE l.id = ?');
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
$libro = mysqli_fetch_assoc(mysqli_stmt_get_result($stmt));
if (!$libro) {
    http_response_code(404);
    $titolo = 'Libro non trovato';
    require 'header.php';
    echo '<h1>Libro non trovato</h1><p><a href="index.php">Torna al catalogo</a></p>';
    require 'footer.php';
    exit;
}

// Storico dei prestiti di questo libro (JOIN con gli studenti)
$stmt = mysqli_prepare($conn, 'SELECT s.nome, s.cognome, s.classe, p.data_prestito, p.data_restituzione
                               FROM prestiti p JOIN studenti s ON s.id = p.studente_id
                               WHERE p.libro_id = ?
                               ORDER BY p.data_prestito DESC');
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
$prestiti = mysqli_fetch_all(mysqli_stmt_get_result($stmt), MYSQLI_ASSOC);
$fuori = count(array_filter($prestiti, fn($p) => $p['data_restituzione'] === null));

$titolo = $libro['titolo'];
require 'header.php';
?>
<h1><?= h($libro['titolo']) ?></h1>
<dl class="scheda">
    <dt>Autore</dt><dd><?= h($libro['nome'] . ' ' . $libro['cognome']) ?> (<?= h($libro['nazionalita']) ?>)</dd>
    <dt>Anno</dt><dd><?= h((string) $libro['anno']) ?></dd>
    <dt>Genere</dt><dd><?= h($libro['genere']) ?></dd>
    <dt>ISBN</dt><dd><?= h($libro['isbn']) ?></dd>
    <dt>Copie</dt><dd><?= $libro['copie'] ?> (disponibili: <?= $libro['copie'] - $fuori ?>)</dd>
</dl>

<div class="azioni">
    <a class="bottone" href="libro_form.php?id=<?= $libro['id'] ?>">Modifica</a>
    <form method="post" action="libro_elimina.php" onsubmit="return confirm('Eliminare questo libro?')">
        <input type="hidden" name="id" value="<?= $libro['id'] ?>">
        <button class="pericolo">Elimina</button>
    </form>
</div>

<h2>Prestiti</h2>
<?php if ($prestiti): ?>
<table>
    <tr><th>Studente</th><th>Classe</th><th>Prestato il</th><th>Restituito il</th></tr>
    <?php foreach ($prestiti as $p): ?>
    <tr>
        <td><?= h($p['nome'] . ' ' . $p['cognome']) ?></td>
        <td><?= h($p['classe']) ?></td>
        <td><?= date('d/m/Y', strtotime($p['data_prestito'])) ?></td>
        <td><?= $p['data_restituzione'] ? date('d/m/Y', strtotime($p['data_restituzione'])) : '<strong>in prestito</strong>' ?></td>
    </tr>
    <?php endforeach; ?>
</table>
<?php else: ?>
    <p>Questo libro non è mai stato prestato.</p>
<?php endif; ?>

<p><a href="index.php">← Torna al catalogo</a></p>
<?php require 'footer.php'; ?>
  • Due query preparate: una per il libro (con la JOIN sull'autore), una per i suoi prestiti (con la JOIN sugli studenti).
  • Le copie fuori si contano in PHP con array_filter, sulle righe già lette: non serve una terza query.
  • h((string) $libro['anno']): h() vuole una stringa, l'anno è un intero. Il cast lo converte.
  • Il pulsante Elimina è un piccolo form POST, non un link (lezione 9: GET per leggere, POST per cambiare).

Per arrivare alla scheda serve un catalogo. Eccone uno semplice, index.php; nella lezione 14 lo arricchiremo con ricerca, filtri e pagine.

<?php
require 'funzioni.php';

$risultato = mysqli_query($conn, 'SELECT l.id, l.titolo, l.anno, a.cognome
                                 FROM libri l JOIN autori a ON a.id = l.autore_id
                                 ORDER BY l.titolo');
$libri = mysqli_fetch_all($risultato, MYSQLI_ASSOC);

$titolo = 'Catalogo';
require 'header.php';
?>
<h1>Catalogo</h1>
<p><a class="bottone" href="libro_form.php">+ Nuovo libro</a></p>
<table>
    <tr><th>Titolo</th><th>Autore</th><th>Anno</th></tr>
    <?php foreach ($libri as $l): ?>
    <tr>
        <td><a href="libro.php?id=<?= $l['id'] ?>"><?= h($l['titolo']) ?></a></td>
        <td><?= h($l['cognome']) ?></td>
        <td><?= h((string) $l['anno']) ?></td>
    </tr>
    <?php endforeach; ?>
</table>
<?php require 'footer.php'; ?>

C e U: un solo form per inserire e modificare

Ci arriviamo in tre passi: prima solo l'inserimento, poi la modifica, infine le due cose nello stesso file.

1. Solo inserimento

Il form è quello della lezione 9: legge i dati, li controlla, e se è tutto a posto invece di stampare "salvato" esegue un INSERT con una query preparata (lezione 11). Il cuore di libro_nuovo.php:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // ... lettura e controlli come nella lezione 9: i valori sono in $libro,
    //     l'anno in $anno (null se non è stato indicato) ...
    if (!$errori) {
        $stmt = mysqli_prepare($conn, 'INSERT INTO libri (isbn, titolo, anno, genere, copie, autore_id) VALUES (?, ?, ?, ?, ?, ?)');
        mysqli_stmt_bind_param($stmt, 'ssisii', $libro['isbn'], $libro['titolo'], $anno,
                               $libro['genere'], $libro['copie'], $libro['autore_id']);
        mysqli_stmt_execute($stmt);
        $id = mysqli_insert_id($conn);          // l'id appena assegnato al libro
        vai("libro.php?id=$id");                // Post/Redirect/Get verso la scheda
    }
}

2. La modifica

Per modificare il libro 3 si apre libro_modifica.php?id=3. Rispetto all'inserimento cambiano solo tre cose:

  1. all'apertura (GET) i campi non sono vuoti: si legge il libro dal database con una SELECT … WHERE id = ? e si mettono i suoi valori in $libro;
  2. al salvataggio si esegue un UPDATE … WHERE id = ? invece dell'INSERT;
  3. l'id deve sopravvivere all'invio del form: lo si mette in un campo nascosto, <input type="hidden" name="id" value="3">, così al POST la pagina sa ancora quale libro sta modificando.

3. Un solo file per tutti e due

I due file sarebbero uguali al 90%: stesso form, stessi controlli. Quando si cambia un controllo bisognerebbe ricordarsi di farlo in entrambi. Per questo si scrive un solo file, libro_form.php, che decide cosa fare guardando se c'è un id:

  • libro_form.php → nessun id → nuovo libro, INSERT;
  • libro_form.php?id=3 → id 3 → modifica del libro 3, UPDATE.
<?php
// Un solo file per AGGIUNGERE (libro_form.php) e MODIFICARE (libro_form.php?id=3) un libro
require 'funzioni.php';

$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
$libro = ['isbn' => '', 'titolo' => '', 'anno' => '', 'genere' => '', 'copie' => 1, 'autore_id' => 0];
$errori = [];

// Modifica: la prima volta (GET) carico il libro dal database
if ($id && $_SERVER['REQUEST_METHOD'] === 'GET') {
    $stmt = mysqli_prepare($conn, 'SELECT * FROM libri WHERE id = ?');
    mysqli_stmt_bind_param($stmt, 'i', $id);
    mysqli_stmt_execute($stmt);
    $libro = mysqli_fetch_assoc(mysqli_stmt_get_result($stmt)) ?? vai('index.php');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // ---- 1. Leggo i dati inviati
    $libro = [
        'isbn'      => preg_replace('/\D/', '', $_POST['isbn'] ?? ''),   // tengo solo le cifre
        'titolo'    => trim($_POST['titolo'] ?? ''),
        'anno'      => trim($_POST['anno'] ?? ''),
        'genere'    => trim($_POST['genere'] ?? ''),
        'copie'     => (int) ($_POST['copie'] ?? 0),
        'autore_id' => (int) ($_POST['autore_id'] ?? 0),
    ];
    // ---- 2. Li controllo: MAI fidarsi di quello che arriva dal browser
    if (strlen($libro['isbn']) !== 13) $errori[] = "L'ISBN deve avere 13 cifre.";
    if ($libro['titolo'] === '') $errori[] = 'Il titolo è obbligatorio.';
    if ($libro['anno'] !== '' && (!ctype_digit($libro['anno']) || $libro['anno'] > date('Y'))) $errori[] = "L'anno non è valido.";
    if ($libro['genere'] === '') $errori[] = 'Il genere è obbligatorio.';
    if ($libro['copie'] < 1 || $libro['copie'] > 50) $errori[] = 'Le copie devono essere tra 1 e 50.';
    if (!$libro['autore_id']) $errori[] = "Scegli l'autore.";

    // ---- 3. Se è tutto a posto salvo (INSERT o UPDATE) e torno alla scheda del libro
    if (!$errori) {
        $anno = $libro['anno'] === '' ? null : (int) $libro['anno'];   // anno vuoto → NULL
        try {
            if ($id) {
                $stmt = mysqli_prepare($conn, 'UPDATE libri SET isbn = ?, titolo = ?, anno = ?, genere = ?, copie = ?, autore_id = ? WHERE id = ?');
                mysqli_stmt_bind_param($stmt, 'ssisiii', $libro['isbn'], $libro['titolo'], $anno,
                                       $libro['genere'], $libro['copie'], $libro['autore_id'], $id);
                mysqli_stmt_execute($stmt);
                messaggio('Libro aggiornato.');
            } else {
                $stmt = mysqli_prepare($conn, 'INSERT INTO libri (isbn, titolo, anno, genere, copie, autore_id) VALUES (?, ?, ?, ?, ?, ?)');
                mysqli_stmt_bind_param($stmt, 'ssisii', $libro['isbn'], $libro['titolo'], $anno,
                                       $libro['genere'], $libro['copie'], $libro['autore_id']);
                mysqli_stmt_execute($stmt);
                $id = mysqli_insert_id($conn);
                messaggio('Libro aggiunto al catalogo.');
            }
            vai("libro.php?id=$id");
        } catch (mysqli_sql_exception $e) {
            // 1062 = valore duplicato in una colonna UNIQUE (qui: l'ISBN)
            if ($e->getCode() === 1062) $errori[] = 'Esiste già un libro con questo ISBN.';
            else throw $e;
        }
    }
}

$autori = mysqli_fetch_all(mysqli_query($conn, 'SELECT id, nome, cognome FROM autori ORDER BY cognome, nome'), MYSQLI_ASSOC);
$titolo = $id ? 'Modifica libro' : 'Nuovo libro';
require 'header.php';
?>
<h1><?= $titolo ?></h1>

<?php if ($errori): ?>
<ul class="errori"><?php foreach ($errori as $e): ?><li><?= h($e) ?></li><?php endforeach; ?></ul>
<?php endif; ?>

<form method="post" class="modulo">
    <input type="hidden" name="id" value="<?= $id ?>">
    <label>Titolo <input name="titolo" value="<?= h($libro['titolo']) ?>" required maxlength="150"></label>
    <label>Autore
        <select name="autore_id" required>
            <option value="">— scegli —</option>
            <?php foreach ($autori as $a): ?>
                <option value="<?= $a['id'] ?>" <?= $a['id'] == $libro['autore_id'] ? 'selected' : '' ?>><?= h($a['cognome'] . ' ' . $a['nome']) ?></option>
            <?php endforeach; ?>
        </select>
    </label>
    <label>ISBN (13 cifre) <input name="isbn" value="<?= h($libro['isbn']) ?>" required inputmode="numeric"></label>
    <label>Anno <input name="anno" type="number" value="<?= h((string) $libro['anno']) ?>" max="<?= date('Y') ?>"></label>
    <label>Genere <input name="genere" value="<?= h($libro['genere']) ?>" required maxlength="30"></label>
    <label>Copie <input name="copie" type="number" min="1" max="50" value="<?= h((string) $libro['copie']) ?>" required></label>
    <button>Salva</button>
    <a href="<?= $id ? "libro.php?id=$id" : 'index.php' ?>">Annulla</a>
</form>
<?php require 'footer.php'; ?>

I punti da capire bene:

  • L'id viaggia in un campo nascosto (<input type="hidden" name="id">): così, quando il form viene inviato, la pagina sa ancora se sta modificando un libro. Per questo l'id si legge da $_GET oppure da $_POST.
  • mysqli_fetch_assoc(...) ?? vai('index.php'): se il libro da modificare non esiste, mysqli_fetch_assoc restituisce null e si torna al catalogo. L'operatore ?? restituisce il valore a sinistra se non è null, altrimenti esegue la parte a destra.
  • L'autore si sceglie da un menu costruito con una query sulla tabella autori: nel value c'è l'id, che è proprio il valore da salvare nella chiave esterna autore_id. L'utente vede il nome, il database riceve il numero.
  • Le lettere dei tipi seguono i ? nell'ordine: 'ssisii' = isbn (s), titolo (s), anno (i), genere (s), copie (i), autore (i). Nell'UPDATE c'è una i in più, in fondo, per l'id del WHERE. L'anno può valere null: MySQL salva NULL.
  • Il vincolo UNIQUE lavora per noi: non controlliamo prima se l'ISBN esiste già. Proviamo a salvare e, se MySQL rifiuta con l'errore 1062 (Duplicate entry, valore duplicato), mostriamo un messaggio chiaro. Il numero dell'errore lo dà $e->getCode(). È più semplice e più sicuro di un controllo fatto prima: tra il controllo e il salvataggio un altro utente potrebbe inserire lo stesso ISBN.
  • Un errore che non ci aspettiamo (else throw $e) non va nascosto: lo si rilancia, e PHP mostra l'errore.

⚠️ L'UPDATE senza WHERE. Se dimentichi WHERE id = ?, il salvataggio di un solo libro cambia tutti i libri del catalogo. Controlla sempre l'UPDATE due volte. E tieni a portata di mano biblioteca.sql per reimportare il database.

D: cancellare

libro_elimina.php non mostra nulla: riceve l'id con POST, cancella e reindirizza.

<?php
// Si arriva qui SOLO con un modulo POST (mai con un link: un link si può aprire per sbaglio)
require 'funzioni.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') vai('index.php');

$id = (int) ($_POST['id'] ?? 0);
try {
    $stmt = mysqli_prepare($conn, 'DELETE FROM libri WHERE id = ?');
    mysqli_stmt_bind_param($stmt, 'i', $id);
    mysqli_stmt_execute($stmt);
    if (mysqli_stmt_affected_rows($stmt) > 0) {
        messaggio('Libro eliminato.');
    } else {
        messaggio('Libro non trovato.', 'errore');
    }
    vai('index.php');
} catch (mysqli_sql_exception $e) {
    // 1451 = la riga è usata da un'altra tabella (chiave esterna)
    // La chiave esterna di "prestiti" impedisce di cancellare un libro che ha prestiti registrati
    messaggio('Non puoi eliminare un libro che ha dei prestiti registrati.', 'errore');
    vai("libro.php?id=$id");
}
  • mysqli_stmt_affected_rows() dice quante righe sono state toccate da un INSERT, UPDATE o DELETE. Se è 0, il libro non c'era (magari già cancellato da un'altra scheda del browser).
  • La chiave esterna protegge lo storico: un libro con prestiti non si può cancellare, perché nella lezione 4 abbiamo scelto ON DELETE RESTRICT. MySQL rifiuta con l'errore 1451, mysqli lancia un'eccezione e noi la trasformiamo in un messaggio comprensibile. Anche qui è il database a fare il controllo: noi gestiamo solo l'esito.
  • La conferma confirm() nel form della scheda è una cortesia per l'utente, non una protezione: si aggira facilmente. La protezione vera è il metodo POST (e, dalla prossima lezione, il login e il token CSRF).

Prova: crea un libro nuovo, poi eliminalo (ci riesci). Poi prova a eliminare 1984: compare il messaggio d'errore, perché ha dei prestiti.

Il salvataggio a oggetti (mysqli)
<?php
$stmt = $conn->prepare('INSERT INTO libri (isbn, titolo, anno, genere, copie, autore_id) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->bind_param('ssisii', $libro['isbn'], $libro['titolo'], $anno, $libro['genere'], $libro['copie'], $libro['autore_id']);
$stmt->execute();
$id = $conn->insert_id;

$stmt = $conn->prepare('DELETE FROM libri WHERE id = ?');
$stmt->bind_param('i', $id);
$stmt->execute();
echo $stmt->affected_rows;   // righe cancellate
Il salvataggio con PDO
<?php
try {
    $stmt = $pdo->prepare('INSERT INTO libri (isbn, titolo, anno, genere, copie, autore_id) VALUES (?, ?, ?, ?, ?, ?)');
    $stmt->execute([$libro['isbn'], $libro['titolo'], $anno, $libro['genere'], $libro['copie'], $libro['autore_id']]);
    $id = $pdo->lastInsertId();
} catch (PDOException $e) {
    // in PDO getCode() restituisce il codice SQLSTATE (una stringa); il numero MySQL è in errorInfo[1]
    if ($e->errorInfo[1] === 1062) $errori[] = 'Esiste già un libro con questo ISBN.';
    else throw $e;
}

$stmt = $pdo->prepare('DELETE FROM libri WHERE id = ?');
$stmt->execute([$id]);
echo $stmt->rowCount();      // righe cancellate

Esercizi

Esercizio 1 – Il CRUD degli studenti. Crea studenti.php (elenco in ordine di classe e cognome, con il link alla modifica) e studente_form.php (inserimento e modifica, con i controlli dell'esercizio 2 della lezione 9). Un'email ripetuta deve dare un messaggio chiaro.

Mostra la soluzione

studente_form.php segue lo schema di libro_form.php. La parte centrale:

<?php
require 'funzioni.php';

$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
$s = ['nome' => '', 'cognome' => '', 'classe' => '', 'email' => ''];
$errori = [];

if ($id && $_SERVER['REQUEST_METHOD'] === 'GET') {
    $stmt = mysqli_prepare($conn, 'SELECT nome, cognome, classe, email FROM studenti WHERE id = ?');
    mysqli_stmt_bind_param($stmt, 'i', $id);
    mysqli_stmt_execute($stmt);
    $s = mysqli_fetch_assoc(mysqli_stmt_get_result($stmt)) ?? vai('studenti.php');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    foreach ($s as $campo => $valore) {
        $s[$campo] = trim($_POST[$campo] ?? '');
    }
    $s['classe'] = strtoupper($s['classe']);
    if ($s['nome'] === '' || $s['cognome'] === '') $errori[] = 'Nome e cognome sono obbligatori.';
    if (!preg_match('/^[1-5][A-Z]$/', $s['classe'])) $errori[] = 'Scrivi la classe così: 4A.';
    if (!filter_var($s['email'], FILTER_VALIDATE_EMAIL)) $errori[] = "L'email non è valida.";

    if (!$errori) {
        try {
            if ($id) {
                $stmt = mysqli_prepare($conn, 'UPDATE studenti SET nome = ?, cognome = ?, classe = ?, email = ? WHERE id = ?');
                mysqli_stmt_bind_param($stmt, 'ssssi', $s['nome'], $s['cognome'], $s['classe'], $s['email'], $id);
            } else {
                $stmt = mysqli_prepare($conn, 'INSERT INTO studenti (nome, cognome, classe, email) VALUES (?, ?, ?, ?)');
                mysqli_stmt_bind_param($stmt, 'ssss', $s['nome'], $s['cognome'], $s['classe'], $s['email']);
            }
            mysqli_stmt_execute($stmt);
            messaggio('Studente salvato.');
            vai('studenti.php');
        } catch (mysqli_sql_exception $e) {
            if ($e->getCode() === 1062) $errori[] = 'Questa email è già usata da un altro studente.';
            else throw $e;
        }
    }
}

Il form HTML ha i quattro campi più l'id nascosto. In studenti.php la query è SELECT id, nome, cognome, classe FROM studenti ORDER BY classe, cognome e ogni riga ha il link studente_form.php?id=….

Esercizio 2 – Gli autori. Crea il form per aggiungere un autore (nome, cognome, nazionalità facoltativa). Se la nazionalità è vuota, nel database deve finire NULL, non una stringa vuota. Come controlli il risultato in phpMyAdmin?

Mostra la soluzione
<?php
$nazionalita = trim($_POST['nazionalita'] ?? '');
if ($nazionalita === '') {
    $nazionalita = null;              // stringa vuota → NULL
}
$stmt = mysqli_prepare($conn, 'INSERT INTO autori (nome, cognome, nazionalita) VALUES (?, ?, ?)');
mysqli_stmt_bind_param($stmt, 'sss', $nome, $cognome, $nazionalita);
mysqli_stmt_execute($stmt);

In phpMyAdmin, nella scheda Mostra della tabella autori, il valore NULL compare in corsivo grigio. Oppure con la query SELECT * FROM autori WHERE nazionalita IS NULL;: una stringa vuota non verrebbe trovata.

La differenza conta: con le stringhe vuote, WHERE nazionalita IS NULL non trova gli autori senza nazionalità e i conteggi COUNT(nazionalita) li contano lo stesso.

Esercizio 3 – Perché RESTRICT? Un compagno propone di cambiare la chiave esterna di prestiti in ON DELETE CASCADE, "così si possono eliminare tutti i libri". Spiega cosa succederebbe eliminando Se questo è un uomo e proponi un'alternativa alla cancellazione per un libro perso o rovinato.

Mostra la soluzione

Con CASCADE, eliminando Se questo è un uomo spariscono anche i suoi 3 prestiti, compresi quelli ancora aperti di Luca Romano e Matteo Ricci: la biblioteca non saprebbe più che quei due studenti devono restituire un libro, e le statistiche cambierebbero anche per il passato.

Un'alternativa comune è non cancellare: si aggiunge una colonna, per esempio ALTER TABLE libri ADD attivo BOOLEAN NOT NULL DEFAULT TRUE;, e "eliminare" diventa UPDATE libri SET attivo = FALSE WHERE id = ?. Il catalogo mostra solo i libri con attivo = TRUE, ma lo storico resta. Si chiama cancellazione logica (soft delete). Per un libro perso si può anche solo diminuire il numero di copie.