PHP 5.1: Abstraktion und Prepared Statements mit PDO
PHP 5.1 ist der große Wurf für alle Freunde der Datenbanken. Endlich macht ein eigenes Abstraktionsmodell unabhängig von der darunter liegenden Engine. Und endlich gibt es Features wie Prepared Statements. Was dahinter steckt, zeigt dieser 2005 in Internet Professionell veröffentlichte Artikel.
Datenbankabstraktion - welch ein Begriff. Als wären Datenbanken nicht schon abstrakt genug. Doch hinter diesem Begriff steckt eine große Arbeitserleichterung. Denn mit der Datenbankabstraktion brauchen Sie sich nur noch um die Abfragesyntax selbst zu kümmern. Die Kommunikation mit der Datenbank übernimmt PHP. Dabei ist es egal, ob es sich um PostgreSQL, MySQL, Oracle oder andere Datenbanken handelt.
Sie brauchen bei einem Umstieg von einem auf das andere System nicht mehr tief im PHP-Code wühlen und jede einzelne Datenbankfunktion ändern. Sie erinnern sich: für MySQL gelten bislang Funktionen, die mit mysql_ beginnen, für Postgres gibt es das Präfix sq_ und so weiter.
Ein PDO-Objekt erzeugen
Bevor Sie mit der Datenbank arbeiten können, erzeugen Sie eine neue Instanz der Klasse PDO. Das sieht in diesem Beispiel so aus:
$strDbLocation = 'mysql:dbname=ipro;host=127.0.0.1';
$strDbUser = 'root';
$strDbPassword = '';
try
{
$objDb = new PDO($strDbLocation, $strDbUser, $strDbPassword);
}
catch (PDOException $e)
{
echo 'Fehler beim Öffnen der Datenbank: ' . $e->getMessage();
}
Zunächst packen Sie Datenbank-Kennung, Benutzernamen und Kennwort in drei Variablen. In diesem Fall arbeiten Sie mit MySQL. Achtung: Der Code im Beispiel ist nur für den Einsatz auf einem Entwicklungsrechner gedacht. Sobald Sie eine Datenbank im Internet oder Intranet zur Verfügung stellen, müssen Sie den Zugang schützen, indem Sie mit einem eigenen Benutzernamen für die Datenbank und einem Kennwort arbeiten.
Danach geht es in ein try-catch-Konstrukt. Hier erzeugen Sie mit new PDO() eine neue Instanz des PHP Data Objects und übergeben es an $objDb.
Mögliche Fehler fängt das catch ab. Um welchen Fehler es sich genau handelt, sagt $e mit $e->getMessage();. Natürlich können Sie in diesem Konstrukt auch noch weitere Aktionen auslösen. Nützlich ist zum Beispiel das Versenden einer E-Mail an den Administrator. So bekommen Sie schnell Bescheid, falls Ihr Datenbankserver ein Problem hat.
Geht alles gut, legen Sie gleich los. Als Beispiel dient eine simple Log-Datenbank mit zwei Feldern: "message" und "prioritaet". Schreiben Sie zum Beispiel:
$objDb->exec("INSERT INTO log (message, prioritaet) VALUES ('Hallo Welt', 0)");
Der Ausdruck erinnert noch ein wenig an die gute alte mysql_query. Sie geben als Parameter einen SQL-Befehl ein und ab geht die Post. Allerdings gibt dieses Verfahren lediglich die Anzahl der von der Änderung betroffenen Einträge zurück - in diesem Beispiel also eine 1. Das macht exec unbrauchbar für den Umgang mit SELECT-Abfragen, die Daten aus einer Tabelle heraus holen.
Dafür eignet sich besser folgende Zeile:
$dbResult = $objDb->query("SELECT * FROM log");
Das Angenehme daran: Durch das Ergebnis $dbresult können Sie sich hangeln wie durch ein Array, etwa:
foreach($dbResult as $row)
{
echo $row[0]." ".$row[1]."<br/>\n";
}
Eine Extra-Funktion wie mysql_fetch_array() ist damit nicht mehr notwendig. Dennoch gibt es ein fetch() und ein fetchAll(). Dazu gleich mehr.
Prepared Statements
Eine schicke Fähigkeit des neuen PHP Data Object: Prepared Statements. Wie der Name schon sagt, sind das "vorbereitete" SQL-Statements. Sie kümmern sich zunächst nur um das Statement an sich und machen sich noch keine Gedanken, woher die Daten aus dem PHP-Skript kommen und wohin sie gehen sollen.
Ist die Abfrage vorbereitet, binden Sie die Bestandteile der Abfrage an Variablen aus Ihrem Skript. Das klingt zunächst ein wenig umständlich, spart aber Zeit, wenn Sie mehrere Abfragen starten. Ein Beispiel: Sie schreiben mehrere Informationen in Ihre Log-Datei. Dazu bereiten Sie das folgende Statement vor:
$dbInsertLog = $objDb->prepare("INSERT INTO log (message, prioritaet) VALUES (:message, :prioritaet)");
Als Parameter der Prepare-Methode finden Sie das SQL-Statement, das später die Log-Datei füllen soll. Hinter VALUES kommen zwei spezielle Einträge mit vorangestellten Doppelpunkten. Und genau hier setzen Sie an. Diesen beiden Einträgen können Sie den Inhalt beliebiger Variablen zuordnen. Das geht mit den Zeilen
$dbInsertLog->bindParam('message', $strNachricht);
$dbInsertLog->bindParam('prioritaet', $intPrioritaet);
Der Wert für :message kommt also aus der Variablen $strNachricht, der Wert für :prioritaet aus $intPrioritaet. Wichtig ist dabei: Bei der Bindung wird nicht einmalig der Inhalt der Variablen übergeben, sondern ein Zeiger auf die Variable. Ändert sich ihr Inhalt, wird beim nächsten Schreibvorgang auch ein neuer Wert in die Datenbank geschrieben.
Nehmen wir an, im Verlauf des Programms geschehen mehrere Ereignisse, die Sie immer im Log mitschreiben möchten. Nichts leichter als das: Sie belegen nur die Variablen $strNachricht und $intPrioritaet neu und führen die Datenbankabfrage aus.
Zum Beispiel:
$strNachricht = 'Datenbankabfrage ausgefuehrt';
$intPrioritaet = 1;
$dbInsertLog->execute();
Die Methode <i>excute()</i> schreibt den aktuellen Inhalt der Variablen in die Datenbank. Danach belegen Sie die Variablen neu und führen dieselbe Methode nochmals aus.
$strNachricht = 'Sensoren rekalibriert';
$intPrioritaet = 2;
$dbInsertLog->execute();
Noch interessanter wird's, wenn Sie massenhaft Informationen in eine Datenbank schreiben wollen, etwa aus einem Array. Dann bauen Sie eine Schleife, um die Daten aus dem Array zu holen und fügen Sie nacheinander über das Prepared Statement in die Datenbank ein. Das kann zum Beispiel so aussehen:
$arrInfo = array(
array(':message' => 'Hallo Welt', ':prioritaet' => 0),
array(':message' => 'Hallo Mond', ':prioritaet' => 1),
array(':message' => 'Hallo Mars', ':prioritaet' => 2),
array(':message' => 'Hallo Jupiter', ':prioritaet' => 3),
array(':message' => 'Hallo Venus', ':prioritaet' => 4),
);
$dbInsert = $objDb->prepare("INSERT INTO log (message, prioritaet) VALUES (:message, :prioritaet)");
foreach($arrInfo as $arrEntry)
{
$dbInsert->execute($arrEntry);
}
Hier verzichten Sie auf ein explizites Binden von Variablen. Stattdessen haben Sie ein assoziatives Array. In dessen Schlüsseln steht genau die Bezeichnung, die Sie im SQL-Statement hinter VALUES finden. Da es sich um ein zweidimensionales Array handelt, müssen Sie sich dort mit einer foreach-Schleife hindurchbewegen und können dann jeden einzelnen Eintrag per execute in die Datenbank schreiben.
Genauso einfach nutzen Sie ein Array, um einzelne Datensätze zu schreiben, etwa
$dbInsert->execute(array(':message' => 'Hallo Pluto', 'prioritaet' => 10));
Prepared Statements in Abfragen
Prepared Statements eignen sich auch, um elegant Abfrageparameter zu übergeben. Ein Beispiel zeigt, wie es geht. Zunächst bereiten Sie das Statement vor:
$dbSelect = $objDb->prepare("SELECT * FROM log WHERE prioritaet >= :prio");
In dieser Zeile finden Sie bereits unseren Abfrageparameter :prio. Den binden Sie in der nächsten Zeile an eine Variable. In diesem Fall an einen per POST übergebenen Wert.
$dbSelect->bindParam('prio', $_POST['prioritaet']);
Danach geht es erneut an das Ausführen der Anweisung und das Anzeigen der Ergebnisse.
$dbSelect->execute();
foreach($dbSelect as $row)
{
echo $row[0].' '.$row[1]."<br/>\n";
}
Auch hier macht es sich wieder sehr angenehm bemerkbar, dass $dbSelect wie ein Array behandelt werden darf. Natürlich kommen Sie auch auf anderen Wegen zum Ergebnis, zum Beispiel mit fetch():
$arrDatensatz = $dbSelect->fetch();
print_r($arrDatensatz);
Damit holen Sie den jeweils nächsten Datensatz in ein Array. Die Werte im Array sind sowohl über die Feldnamen erreichbar, wie auch über einen numerischen Index. Die Ausgabe sieht in diesem Fall so aus:
Array
(
[message] => Hallo Welt
[0] => Hallo Welt
[prioritaet] => 1
[1] => 1
)
Soll das Array ausschließlich numerisch indexiert sein, probieren Sie es damit:
$arrDatensatz = $dbSelect->fetch(PDO_FETCH_NUM);
print_r($arrDatensatz);
Um das komplette Abfrageergebnis auf einen Rutsch in ein Array zu übertragen, verwenden Sie fetchAll():
$arrResult = $dbSelect->fetchAll();
print_r($arrResult);
In $arrResult steckt dann ein riesiges Array mit dem Abfrageergebnis. Auch hier sind die Daten über numerische Schlüssel und über die Namen der Spalten zu erreichen.
Spalten an Variablen binden
Nicht nur bei Abfragen oder dem Einfügen der Daten funktioniert das Binden von Variablen. Genauso gut dürfen Sie Spalten an Variablen binden. Sprich: Beim Auslesen des Abfrageergebnisses erhält eine zuvor festgelegte Variable den Eintrag aus der Datenbank.
In der Praxis sieht das so aus:
$dbSelect = $objDb->prepare("SELECT DISTINCT message, prioritaet FROM log WHERE prioritaet <= :prio");
$dbSelect->bindParam('prio', $_POST['prioritaet']);
$dbSelect->execute();
$dbSelect->bindColumn('message', $strNachricht);
foreach($dbSelect as $row)
{
echo $row[0].' '.$row[1]."<br/>\n";
echo 'Und hier noch eine Nachricht: ->'.$strNachricht."<-<br/>\n";
}
Nach dem Ausführen der Abfrage bindet bindColumn() den Inhalt der Spalte message an die Variable $strNachricht. Die Reihenfolge "erst Abfrage, dann Bindung" empfiehlt die PHP-Dokumentation aus Gründen der Portabilität.
Im Beispiel arbeiten danach beide Mechanismen: foreach holt die Daten nacheinander aus dem Ergebnis. Der erste Echo-Befehl holt die Einträge wie gehabt aus dem Array $row. Zusätzlich aber erhält in jedem Durchlauf die Variable $strNachricht den Wert des aktuellen Inhalts von message. Die zweite Echo-Zeile gibt genau deren Inhalt aus.
Dieses Verfahren spart vor allem Arbeit: Statt jedes Mal von Hand den Wert mit
$strNachricht = $row[0];
zuzuweisen, steht der Wert sofort zur Verfügung.
Transaktionen
Transaktionen sind ein wichtiger Sicherheitsfaktor in Datenbanken. Denn sie schützen vor versehentlichen Datenverlusten und ersparen Reparaturarbeiten.
In einer Transaktion führen Sie eine oder mehrere Arbeiten zunächst unter geschützten Bedingungen aus. Sollte etwas schief gehen, haben Sie die Möglichkeit, einen Rollback auszuführen. Sprich: alle bis dahin vorgenommenen Datenbankbefehle lassen sich auf einen Schlag rückgängig machen. Nachteil des Ganzen: es funktioniert nicht bei allen Tabellentypen. Sie brauchen eine Datenbank oder einen Tabellentyp, der Transaktionen unterstützt. Bei MySQL kann das zum Beispiel der Tabellentyp BDB (Berkeley DB) sein - mit den gängigen MyISAM-Tabellen funktioniert das leider nicht.
Folgendes Beispiel zeigt das Prinzip:
$objDb->beginTransaction();
$objDb->exec("INSERT INTO log (message, prioritaet) VALUES ('Hallo Welt', 1)");
$intIsError += $objDb->errorCode();
$objDb->exec("INSERT INTO log (message, prioritaet) VALUES ('Fehlermeldung', 1)");
$intIsError += $objDb->errorCode();
if ($intIsError === 0) {
$objDb->commit();
}
else {
$objDb->rollBack();
}
Mit beginTransaction() leiten Sie die Transaktion ein. Darauf folgen zwei Statements. Nach jedem Statement fragt das Programm den Errorcode ab. Der ist 0, wenn der SQL-Befehl erfolgreich war. Anderenfalls gibt er einen Wert größer 0 zurück. Dieser Errorcode wird bei den zwei Statements aufsummiert. Ergibt sich danach ein Wert gleich 0, so führt commit() die beiden Statements endgültig aus. Ist der Wert ungleich 0, so macht rollBack() alles wieder rückgängig.
Fazit
Die Auseinandersetzung mit dem PHP Data Object lohnt sich. Vieles im Umgang mit Datenbanken wird einfacher. Besonders bei neuen Projekten sollten Sie überlegen, ob Sie nicht von vornherein auf PDO setzen.
Links
- PHP: String als Variablennamen zuweisen
- Templates mit Smarty gestalten und nutzen
- Variablen in Strings einbinden
- Kennwortschutz und Authentifizierung in PHP
- Templates mit PHP
- Variablentyp in PHP feststellen
- Ein Blog mit Access, XML und XSLT
- Dynamische Menüs mit Javascript
- Reguläre Ausdrücke in der Praxis
- PHP5 Beta: Kein Problem mit MySQL
blog comments powered by Disqus
http://wiki.cc/php/PDO ist tot - schade.
Insgesamt fand den Artikel zu PDO zwar recht hilfreich und informativ
aber wie auch anderswo vermisse ich ein Eingehen auf signifikante
Schwachstellen, wie etwa die offensichtliche Unmöglichkeit, Subqueries
oder Joins an MySql zu übergeben (was in einer Konsolenanwendung oder
C problemlos funktioniert). Und wenn sie nicht gestorben sind,
versuchen's sie's immer noch.
freundliche Grüße
[andreas theissen | 12.01.2010]

RSS Abonnieren
Facebook
Del.icio.us
Twitter