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, cheheader.phpmostra 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:
- 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; - al salvataggio si esegue un
UPDATE … WHERE id = ?invece dell'INSERT; - 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$_GEToppure da$_POST. mysqli_fetch_assoc(...) ?? vai('index.php'): se il libro da modificare non esiste,mysqli_fetch_assocrestituiscenulle 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: nelvaluec'è l'id, che è proprio il valore da salvare nella chiave esternaautore_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'UPDATEc'è unaiin più, in fondo, per l'id delWHERE. L'anno può valerenull: MySQL salvaNULL. - Il vincolo
UNIQUElavora 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'UPDATEdue volte. E tieni a portata di manobiblioteca.sqlper 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 unINSERT,UPDATEoDELETE. 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.