Questo
post fa parte di una serie preparata qualche anno fa per
delle lezioni su PHP.
Cos'è PDO
PDO (Php Data Objects) è un'estensione che fornisce un'interfaccia coerente per l'accesso a diversi tipi di basi di dati. Si usa specificando quale driver usare per l'accesso alla particolare base di dati che si intende utilizzare.Driver disponibili
L'elenco dei driver disponibili si può ottenere con:<?php
print_r(PDO::getAvailableDrivers());
Sul mio calcolatore ottengo:
Array
(
[0] => mysql
[1] => odbc
[2] => pgsql
[3] => sqlite
[4] => sqlite2
)
Gli esempi che seguono fanno uso, per semplicità, del driver sqlite2, ma a parte la specificazione iniziale della sorgente dati dovrebbero essere applicabili con qualsiasi tipo di base di dati supportata.
Inserimento di righe
Per inserire righe in una tabella dovremo fare una connessione e poi eseguire una query di tipo insert:<?php
try
{
/*** connect to SQLite database and get a handler ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
// $dbh = new PDO("mysql:host=localhost; dbname=mydb", $user, $password); // for MySql
$count = $dbh->exec("INSERT INTO Category(Description, Rank) VALUES ('Libri', 10)");
echo 'Affected rows: ' . $count . "\n";
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Affected rows: 1
Selezione
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$sql = "SELECT * FROM Category";
foreach ($dbh->query($sql, PDO::FETCH_ASSOC) as $row)
{
print_r($row);
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
Array
(
[id] => 2
[Description] => Persone
[Rank] => 2
)
Array
(
[id] => 3
[Description] => Oggetti
[Rank] => 3
)
Array
(
[id] => 4
[Description] => Libri
[Rank] => 10
)
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
/*** we prepare the generic query, using placeholders ***/
$stmt = $dbh->prepare(
"INSERT INTO Category (Description, Rank) VALUES (:description, :rank)"
);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':rank', $rank);
// insert one row
$description = 'Feste';
$rank = 13;
$stmt->execute();
// insert a second row
$description = 'Sport';
$rank = 14;
$stmt->execute();
// insert a third row
$description = '€urozona';
$rank = 15;
$stmt->execute();
// insert a fourth row
$description = "Prove d'orchestra";
$rank = 16;
$stmt->execute();
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Dopo l'esecuzione, possiamo verificare la presenza in tabella delle righe inserite:
$ echo "select * from Category;" | sqlite pictures.sql.db
1|Luoghi|9
2|Persone|2
3|Oggetti|3
4|Libri|10
5|Feste|13
6|Sport|14
7|€urozona|15
8|Prove d'orchestra|16
Analogamente, avremmo potuto fare un collegamento basato sulla posizione anziché sul nome del segnaposto (pratica sconsigliabile):
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare(
"INSERT INTO Category (Description, Rank) VALUES (?, ?)"
);
$stmt->bindParam(1, $description);
$stmt->bindParam(2, $rank);
// insert one row
$description = "Fuochi d'artificio";
$rank = 17;
$stmt->execute();
// insert another row
$description = "Monumenti";
$rank = 18;
$stmt->execute();
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Controlliamo l'esito:
$ echo "select * from Category where id>8;" | sqlite pictures.sql.db
9|Fuochi d'artificio|17
10|Monumenti|18
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare("SELECT * FROM Category where Rank <= ?");
$stmt->bindParam(1, $max_rank);
$max_rank = 10;
if ($stmt->execute())
{
while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
print_r($row);
}
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
Array
(
[id] => 2
[Description] => Persone
[Rank] => 2
)
Array
(
[id] => 3
[Description] => Oggetti
[Rank] => 3
)
Array
(
[id] => 4
[Description] => Libri
[Rank] => 10
)
E anche per la selezione vale ovviamente la possibilità di usare dei segnaposto con nomi sensati:
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare(
"SELECT * FROM Category WHERE Description LIKE :description AND Rank <= :max_rank"
);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':max_rank', $max_rank);
$max_rank = 9;
$description = 'L%';
if ($stmt->execute())
{
while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
print_r($row);
}
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
$stmt = $dbh->prepare(
"UPDATE Picture SET Path=:path WHERE id=:id;"
);
oppure
$stmt = $dbh->prepare(
"DELETE FROM Picture WHERE id=:id;"
);
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$sql = "SELECT * FROM Category";
foreach ($dbh->query($sql, PDO::FETCH_ASSOC) as $row)
{
print_r($row);
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
Array
(
[id] => 2
[Description] => Persone
[Rank] => 2
)
Array
(
[id] => 3
[Description] => Oggetti
[Rank] => 3
)
Array
(
[id] => 4
[Description] => Libri
[Rank] => 10
)
Inserimenti con Prepared Statement
Supponiamo di dover fare diverse operazioni di inserimento in un'unica tabella. A tale scopo è possibile usare i cosiddetti Prepared Statement, con i seguenti vantaggi:- protezione da SQL injection
- maggiore velocità di esecuzione
- maggiore comodità di esecuzione (non dobbiamo preoccuparci di fare l'escape degli apici, ad esempio)
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
/*** we prepare the generic query, using placeholders ***/
$stmt = $dbh->prepare(
"INSERT INTO Category (Description, Rank) VALUES (:description, :rank)"
);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':rank', $rank);
// insert one row
$description = 'Feste';
$rank = 13;
$stmt->execute();
// insert a second row
$description = 'Sport';
$rank = 14;
$stmt->execute();
// insert a third row
$description = '€urozona';
$rank = 15;
$stmt->execute();
// insert a fourth row
$description = "Prove d'orchestra";
$rank = 16;
$stmt->execute();
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Dopo l'esecuzione, possiamo verificare la presenza in tabella delle righe inserite:
$ echo "select * from Category;" | sqlite pictures.sql.db
1|Luoghi|9
2|Persone|2
3|Oggetti|3
4|Libri|10
5|Feste|13
6|Sport|14
7|€urozona|15
8|Prove d'orchestra|16
Analogamente, avremmo potuto fare un collegamento basato sulla posizione anziché sul nome del segnaposto (pratica sconsigliabile):
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare(
"INSERT INTO Category (Description, Rank) VALUES (?, ?)"
);
$stmt->bindParam(1, $description);
$stmt->bindParam(2, $rank);
// insert one row
$description = "Fuochi d'artificio";
$rank = 17;
$stmt->execute();
// insert another row
$description = "Monumenti";
$rank = 18;
$stmt->execute();
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Controlliamo l'esito:
$ echo "select * from Category where id>8;" | sqlite pictures.sql.db
9|Fuochi d'artificio|17
10|Monumenti|18
Selezione tramite Prepared Statement
Anche per le operazioni di selezione possiamo tranquillamente usare delle istruzioni preparate:<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare("SELECT * FROM Category where Rank <= ?");
$stmt->bindParam(1, $max_rank);
$max_rank = 10;
if ($stmt->execute())
{
while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
print_r($row);
}
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
Array
(
[id] => 2
[Description] => Persone
[Rank] => 2
)
Array
(
[id] => 3
[Description] => Oggetti
[Rank] => 3
)
Array
(
[id] => 4
[Description] => Libri
[Rank] => 10
)
E anche per la selezione vale ovviamente la possibilità di usare dei segnaposto con nomi sensati:
<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$stmt = $dbh->prepare(
"SELECT * FROM Category WHERE Description LIKE :description AND Rank <= :max_rank"
);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':max_rank', $max_rank);
$max_rank = 9;
$description = 'L%';
if ($stmt->execute())
{
while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
print_r($row);
}
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Array
(
[id] => 1
[Description] => Luoghi
[Rank] => 9
)
Aggiornamenti e cancellazioni
Istruzioni di tipo update e di tipo delete possono essere preparate in maniera analoga.$stmt = $dbh->prepare(
"UPDATE Picture SET Path=:path WHERE id=:id;"
);
oppure
$stmt = $dbh->prepare(
"DELETE FROM Picture WHERE id=:id;"
);
Gestione degli errori
Nel caso di esecuzione di inserimenti, possiamo controllare l'esito dell'operazione e verificare cosa è andato storto. Si consideri il seguente esempio, in cui tentiamo di inserire una riga in cui il valore per una chiave esterna non è definito:<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$count = $dbh->exec(
"INSERT INTO Picture(Path, Description, Type) VALUES
('pictures/first.jpg', 'A tree', 'jpeg');"
);
if ($count)
{
echo "Elemento inserito\n";
}
else
{
print_r($dbh->errorInfo());
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
L'esecuzione non effettua nessun inserimento, ma possiamo avere notizie sul motivo:
Array
(
[0] => 23000
[1] => 19
[2] => Picture.Category_id may not be NULL
)
Transazioni
Nel caso si debbano eseguire più query dipendenti l'una dall'altra, è consigliabile racchiuderle in una transazione, in modo che vengano eseguite tutte o nessuna. Si consideri questo esempio, in cui prima viene inserita una categoria e successivamente una immagine con un id duplicato e per questo non valido. Ciò che si vuole ottenere è che se il secondo inserimento fallisce, anche il primo deve essere annullato.<?php
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$dbh->beginTransaction();
// we insert a new category
$dbh->exec(
"INSERT INTO Category(Description, Rank) VALUES
('Alberi', 18);"
);
// we get the id, since it comes from an auto-increment field
$category_id = $dbh->lastInsertId();
echo 'Categoria inserita, id=' . $category_id . "\n";
// we insert the picture, using the category_id we got
$count = $dbh->exec(
"INSERT INTO Picture(id, Path, Description, Type, Category_id) VALUES
(1, 'pictures/first.jpg', 'A tree', 'jpeg', $category_id);"
);
if ($count)
{
echo "Elemento inserito\n";
$dbh->commit();
}
else
{
print_r($dbh->errorInfo());
// since we did not insert the picture, we don't want the category either
$dbh->rollBack();
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
L'esecuzione ci dice che la categoria è stata inserita, ma poi in effetti non ne troviamo traccia nel database (visto che è stato eseguito il RollBack):
Categoria inserita, id=11
Array
(
[0] => 23000
[1] => 19
[2] => PRIMARY KEY must be unique
)
Controlliamo il database:
$ echo "select * from Category;" | sqlite pictures.sql.db
...
10|Monumenti|18
Oggetti come risultato
Con fetchObject si possono ottenere i dati direttamente come istanza di una classe:
<?php
class Picture
{
// settings, variables, functions...
}
try
{
/*** connect to SQLite database ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
$query = "SELECT * FROM Picture";
$result = $dbh->query($query);
while ($picture = $result->fetchObject('Picture'))
{
print_r($picture);
}
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
}
Risultato:
Picture Object
(
[id] => 1
[Path] => pictures/first.jpg
[Description] => A tree
[Type] => jpeg
[Width] =>
[Height] =>
[Category_id] => 12
)
Picture Object
(
[id] => 2
[Path] => pictures/first.jpg
[Description] => A tree
[Type] => jpeg
[Width] =>
[Height] =>
[Category_id] => 13
)
Se si usano i prepared statement, si può ovviare forzando il caricamento delle proprietà dopo l'esecuzione del costruttore, come nel seguente esempio:
class Category
{
private $id;
private $Description;
private $Rank;
private $_status;
public function __construct($status)
{
$this->_status = $status;
}
}
try
{
/*** connect to SQLite database and get a handler ***/
$dbh = new PDO("sqlite2:pictures.sql.db");
}
catch(PDOException $e)
{
echo 'Error: ' . $e->getMessage() . "\n";
$dbh = false;
}
if($dbh)
{
$stmt = $dbh->prepare("SELECT * FROM Category");
$stmt->setFetchMode(
PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE,
PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE,
'Category',
array(1)
);
if ($stmt->execute())
{
while ($row = $stmt->fetch())
{
print_r($row);
}
}
}
Risultato:
Category Object
(
[id:Category:private] => 1
[Description:Category:private] => Luoghi
[Rank:Category:private] => 9
[_status:Category:private] => 1
)
Category Object
(
[id:Category:private] => 2
[Description:Category:private] => Persone
[Rank:Category:private] => 2
[_status:Category:private] => 1
)
Category Object
(
[id:Category:private] => 3
[Description:Category:private] => Oggetti
[Rank:Category:private] => 3
[_status:Category:private] => 1
)
Category Object
(
[id:Category:private] => 4
[Description:Category:private] => Libri
[Rank:Category:private] => 10
[_status:Category:private] => 1
)
Nessun commento:
Posta un commento