Leseprobe

Transcription

Leseprobe
BACKEND
Alternativen zur Datenbank-Schnittstelle ext/mysql
Zeit für frischen Wind
Die klassischen Funktionen für den Zugriff auf MySQL-Datenbanken werden in einer der nächsten PHPVersionen nicht mehr enthalten sein. Es gibt jedoch moderne Alternativen. Von Markus Schraudolph
AUTOR
Markus Schraudolph
ist Software-Entwickler, Buchautor und Berater mit den Schwerpunkten Webentwicklung und
Webtechnologien.
Inhalt
Alternativen zur DatenbankSchnittstelle ext/mysql.
D
ie Unterstützung der Datenbank
MySQL, also die Bibliothek ext/
mysql, gehört zu den ältesten Elementen von PHP und wurde ursprünglich für die MySQL-Version
3.2.3 entwickelt. Zwar erfuhr die Library im Lauf der Zeit immer wieder kleinere Updates und Ergänzungen, sie blieb aber in den Möglichkeiten beim Stand von MySQL
4.1 stehen (Bild 1). Nun ist beschlossen worden, dass mit PHP 5.5 diese
Altlast den Status deprecated erhält,
also bei Gebrauch entsprechende
Fehlermeldungen erzeugt und in einer späteren Version von PHP ganz
entfallen soll (Bild 2).
LISTING 1: EXT/MYSQL
mysql_connect('localhost', 'myuser', 'mypwd');
mysql_select_db('mydb');
$result = mysql_query('select * from book');
while ($row = mysql_fetch_array($result)){
echo $row['title'] . "<br>";
}
mysql_close();
LISTING 2: MYSQLI
$dbh = mysqli_connect('localhost', 'myuser',
'mypwd');
mysqli_select_db($dbh,'mydb');
$result = mysqli_query($dbh,'select * from
book');
while ($row = mysqli_fetch_array($result)) {
Die Abhilfe ist lange schon da
Die drei MySQL-APIs von PHP
im Vergleich (Bild 1)
114
echo $row['title'] . "<br>";
}
mysqli_close($dbh);
Schon seit PHP 5.0 gibt es die alternative Bibliothek mysqli. Wobei
das i darin für improved steht. Der
Bis auf eine Kleinigkeit können Sie dieses Beiwohl augenfälligste Unterschied ist die Dualität
spiel durch reine Textersetzung auf die neueihrer Auslegung. Denn Sie können mysqli sore Schnittstelle umbauen. Denn die prozedurawohl klassisch prozedural als auch objektorienle Variante von mysqli verfügt über alle Funktiert einsetzen. Sehen wir uns im Listing 1 einmal
tionsnamen des Vorgängers, nur dass man den
eine ganz simple Nutzung der Datenbank in eiBuchstaben i ans Ende des ersten Namensteils
nem Skript nach der alten Methode an.
anhängt. Aus mysql_connect() wird somit mysqli_connect().
Wenn Sie das Beispiel entsprechend
umbauen, wird das Skript allerdings
noch nicht laufen. Denn mysqli fordert für alle Aktionen die Angabe eines Handles für die gewählte Datenbank. Das war zwar mit den alten mysql-Funktionen auch möglich, ohne
Angabe des Handles haben sie aber
einfach das zuletzt verwendete Handle genutzt. Aus Bequemlichkeit haben
die meisten Programmierer darum die
kürzere Variante eingesetzt.
Die auf mysqli umgestrickte Version
sieht aus wie in Listing 2 gezeigt.
Der Funktionsaufruf mysqli_fetch_
array() benötigt das Handle natürlich
nicht, weil der sich Bezug auf die Datenbank ja durch die in $result referenzierte Abfrage ergibt.
6/2013
BACKEND
LISTING 3: OOP-VERSION
$db = new mysqli('localhost',
'myuser', 'mypwd');
$db->select_db('mydb');
$result = $db->query('select * from
book');
while ($row = $result->
fetch_array()) {
echo $row['title'] . "<br>";
}
$db->close();
Wenn Sie also Ihre Quelltexte durch simples Suchen und Ersetzen plus des Einfügens eines Datenbank-Handles in die Aufrufe umstellen, sind Sie wenigstens vor der drohenden Einstellung von mysql sicher. Allerdings lohnt es
sich, mysqli näher anzusehen, weil es doch einiges mehr bietet.
OOP-Interface
Wer es gewohnt ist, eigene Software im OOP-Stil
zu schreiben, der ist um jede Bibliothek froh, die
sich ebenfalls dieses Programmierstils bedient.
Bei mysqli hat man eben die freie Wahl zwischen beiden Welten. Die Umsetzung ist auch
ganz logisch gestaltet: Beim Erzeugen eines mysqli-Objekts wird die DB-Verbindung hergestellt. Alle darauf anwendbaren Methoden entsprechen genau dem zweiten Namensteil des
prozeduralen Funktionsnamens. Aus mysqli_
query() wird also $obj->query(), wobei $obj ein
mysqli-Objekt sein muss. Das prozedural gelöste mysqli-Beispiel sieht in der OoP-Version dann
so aus, wie in Listing 3 gezeigt.
Dreh- und Angelpunkt ist hier also kein DBHandle, sondern das mysqli-Objekt. Das Resultat der Query ist ein Result-Objekt, das alle Methoden anbietet, die man mit dem alten mysqlInterface auf eine Abfrage anwenden kann, wie
das soeben verwendete fetch_array() oder num_
rows. Letzteres ist allerdings nicht als Methode realisiert, sondern als Instanzvariable, darum
fehlen die Klammern. Sie müssen also zum Beispiel schreiben:
echo " Ergebnis hat " .
$result->num_rows . " Datensätze";
Die Zeile zur Auswahl der Datenbank können
Sie sich sparen und die gewünschte DB einfach
als vierten Parameter im Konstruktor angeben:
$db = new mysqli('localhost',
'myuser', 'mypwd', 'mydb');
Das Prinzip eines Prepared Statements ist, dass
man dem DB-Server ein Kommando in zwei
6/2013
Schritten sendet. Zuerst kommt das SQL-Kommando, bei dem aber alle variablen Teile nur mit
Platzhaltern bestückt sind. Im nächsten Schritt
teilt man dem Server mit, dass er das Kommando ausführen soll, und gibt alle konkreten Werte für die Platzhalter in einer Liste mit.
In früheren Zeiten war diese Methode vor
allem dazu gedacht, eine Reihe gleichartiger,
nacheinander gegebener Kommandos zu beschleunigen. Denn Sie können den zweiten
Schritt beliebig oft mit verschiedenen Wertelisten wiederholen. Weil der Server die Analyse
des SQL-Befehls nur einmal zu Beginn durchführen muss, ist diese Methode schneller.
Das Ende der alten Erweiterung
zum Zugriff auf MySQL-Datenbanken ist eingeläutet (Bild 2)
Gestiegene Sicherheitsanforderungen
Mit den gestiegenen Sicherheitsanforderungen an Websites kommt aber noch ein weiterer
wichtiger Punkt dazu, der für die Verwen- ▶
LISTING 4: MASSEN-INSERT
$import = array(
array('title' => 'I Robot', 'author_id' => 4711),
array('title' => 'Der kleine Prinz', 'author_id' => 2025)
);
$db = new mysqli('localhost', 'myuser', 'mypwd', 'mydb');
$sql = "INSERT INTO book(title,author_id) VALUES (?,?)";
if (!($stmt = $db->prepare($sql))) {
echo "Prepare failed: " . $db->error;
}
foreach($import as $book) {
if (!$stmt->bind_param(
"si",$book['title'],$book['author_id'])) {
echo "Bind-Fehler: " .$stmt->error;
}
if (!$stmt->execute()) {
echo "Execute-Fehler: ".$stmt->error;
}
}
$stmt->close();
115
BACKEND
Diese Datenbanktreiber beherrscht PDO derzeit (Bild 3)
dung von Prepared Statements spricht: Wenn
Sie konsequent dieses Konzept nutzen, dann ist
die SQL-Injection kein Einfallstor mehr zu Ihrer
Anwendung. Denn diese Hintertür basiert darauf, dass man in übergebene Parameter SQLKommandos einschleust. Im ersten Schritt wird
der Befehl aber bereits interpretiert.
Ganz gleich welche gewieften Kommandofolgen in den Parametern eingeschleust werden: Der DatenbankServer wird diese immer als Parameter verwenden und somit
vielleicht einen Fehler
melden, weil die Daten
nicht zum erwarteten
Format passen, aber er
wird nie von sich aus
etwas Unerwünschtes
ausführen.
Ein Beispiel: Wenn
es Ihre Aufgabe ist,
aus einer vorliegenden
Textdatei Buchtitel in
die Datenbank zu importieren, dann nutzen Sie dazu einmalig die
Methode prepare() und für jeden einzufügenden
Datensatz ein execute().
Nehmen wir an, die importierten Daten stehen fertig bereit im Array $import, im Beispiel
legen wir das einfach einmal statisch fest. Dann
könnte ein solches Massen-Insert in etwa wie in
Listing 4 gelöst werden.
Interessant wird es hier schon bei der Definition des SQL-Strings, der hier der Übersichtlichkeit wegen in eine eigene Variable ausgelagert
ist. Die dort auftauchenden beiden Fragezeichen
sind die Platzhalter für die variablen Daten, die
später erst eingebaut werden. Obwohl das Feld
title für den Buchtitel sicherlich eine Zeichenkette darstellt, müssen Sie den ersten Platzhalter
nicht in Anführungszeichen setzen. Das passiert
automatisch bei der Wertzuweisung.
Werteliste festlegen
Die Methode prepare() liefert ein Objekt der Klasse mysqli_stmt zurück. Mit der Methode bind_param() legen Sie dann eine Werteliste fest. Dabei
gibt der erste Parameter immer an, welche Typen die einzelnen Variablen haben sollen. Ein
s steht für einen String, ein i für einen IntegerWert, d für eine Fließkommazahl und b für einen BLOB, also einen großen Datenblock, wie
etwa für Bilder.
Mit execute() führen Sie dann den vordefinierten Insert-Befehl mit den aktuellen Werten aus.
Auch alle weiteren Schleifendurchgänge bestehen aus einem bind_param(), gefolgt von execute().
Eine Tatsache bei der Verwendung von Prepared Statements in mysqli fällt schnell ins Auge:
Weil die Platzhalter lediglich über die Reihenfolge ihres Auftretens zugewiesen werden, ist bei
einer Vielzahl von übergebenen Variablen das
Chaos vorprogrammiert. Auch bei Änderungen
TRANSAKTIONEN – DAS SICHERHEITSNETZ
Transaktionen sind eine sehr komfortable Einrichtung, die mit der alten Bibliothek mysql nicht
nutzbar war.
bestellungen...");
Sie können vor der Ausführung einer Reihe von
Aktionen eine Marke setzen. Schlägt dann eine
der Aktionen fehl, müssen Sie in Ihrem Programm
nicht alle bisherigen Datenänderungen einzeln zurücknehmen, sondern geben einfach einen Rollback-Befehl, der dies für Sie macht und alle Änderungen seit dem Setzen des Markers zurückfährt. Hat dagegen alles geklappt, dann geben Sie
nach der letzten Aktion einen Commit-Befehl, der
die Transaktion abschließt.
Das folgende Skript zeigt die Nutzung von
Transaktionen. Es werden zwei SQL-Abfragen abgesetzt, die Daten ändern. Die erste könnte zum
Beispiel eine neue Bestellung anlegen und die
zweite im zugehörigen Kunden-Datensatz die Anzahl der Bestellungen um eins hochsetzen:
}
if($db->errno(){
$db->rollback();
return -1;
$db->autocommit(FALSE);
$db->query("INSERT INTO
116
$db->query("UPDATE kunden...");
Bedenken Sie noch, dass nach dem Commit automatisch die nächste Transaktion beginnt.
Wenn Sie nicht möchten, dass der nächste Rollback alle seit diesem Befehl getroffenen Änderungen ausradiert, sollten Sie den Autocommit
nach erfolgreicher Aktion wieder einschalten:
if($db->errno(){
$db->rollback();
$db->autocommit(TRUE);
return -1;
}
$db->commit();
return 1;
Die erste Zeile des Skripts schaltet das sogenannte Autocommit aus. Das bewirkt ansonsten, dass
die Datenbank nach jeder einzelnen Query selbstständig einen Commit ausführt. Mit dem Kommando wird gleichzeitig die Transaktion begonnen.
Tritt ein Fehler auf, wird der Rollback angestoßen, der alle Änderungen zurücknimmt und den
aktuellen Kontext mit einem Fehlerstatus verlässt.
Klappt dagegen alles, führt das Skript einen Commit aus und beendet damit die Transaktion.
Bei der Nutzung von MySQL als Datenbank-Server funktionieren Transaktionen nur bei Tabellen,
die die InnoDB-Engine nutzen. Falls Sie Transaktionen verwenden möchten, müssen Sie beim Anlegen einer Tabelle explizit den Typ InnoDB wählen, weil die standardmäßige Speicher-Engine
MyISAM ist:
CREATE TABLE auftrag (...)
ENGINE=INNODB;
Besteht die betreffende Tabelle bereits, können
Sie auch ganz einfach die Engine noch nachträglich wechseln.
6/2013
BACKEND
am SQL-Kommando muss man peinlich genau
darauf achten, dass die richtige Zuordnung der
Werteliste gewahrt bleibt.
Der nächste Schritt: PDO
Mit PDO steht Ihnen seit PHP 5.1 eine weitere
moderne Möglichkeit offen, mit Datenbanken
zu kommunizieren. Der größte Vorteil dabei:
Der Austausch der Datenbank ist damit recht
einfach möglich, weil PDO so viel wie möglich
abstrahiert, also von der konkret eingesetzten
DB unabhängig macht.
Wenn sich also im Lauf der Zeit herausstellt,
dass der Umstieg auf eine andere Datenbank
notwendig ist, müssen Sie im Idealfall nur den
Servertyp im Connection-String ändern und die
wenigen Queries anpassen, die ganz spezielle
Funktionen nutzen.
PDO ist auch kein alternativer Treiber, sondern nutzt seinerseits Treiber wie mysqli für
seine Arbeit (Bild 3). Sehen wir uns das einfache Abfrage-Beispiel in der Version für PDO an
(Listing 5).
Der Konstruktor des Objekts erwartet als ersten Parameter einen DSN (Data Source Name).
Dieser legt alle notwendigen Informationen fest,
um den Server ansprechen zu können. Er besteht
immer aus dem Namen der Datenbank, gefolgt
von den Zugangsinfos. Deren Syntax ist von
der jeweiligen Datenbank abhängig. Die Verbindung zu einer Sqlite-Datenbank sieht beispielsweise sinngemäß so aus:
Bis auf die Nutzung von Exceptions ist der Aufbau des
Skripts völlig identisch zu der
Variante mit mysqli. Bei bindParam() sehen Sie gut, wie die
Zuordnung über den Namen
gelöst ist.
Der dritte Parameter bei der
Bindung gibt immer den erwarteten Datentyp an. Lassen Sie ihn weg, wandelt PDO
den Parameter einfach in einen
String um.
Wenn der Typ String für die
von Ihnen verwendeten Parameter in Ordnung ist, dann
können Sie auch eine kompaktere Version der Bindung verwenden und alle Parameter
der execute-Methode in einem
Rutsch übergeben:
LISTING 5: PDO
$db = new PDO('mysql:dbname=mydb;
host=localhost','myuser','mypwd');
foreach ($db->query('select * from
book') as $row) {
echo $row['title'] . "<br>";
}
LISTING 6: PDO-ALTERNATIVE
$stmt = $db->prepare('select *
from book');
$stmt->execute();
while ($row = $stmt->fetch()){
echo $row['title'] . "<br>";
}
$stmt->execute(array(
':title'=>$book['title'],
':author_id'=>$book['author_id']
));
Die Bindung lässt sich in diesem Fall sogar noch
weiter vereinfachen. Denn wenn Sie execute()
ein Array als Parameter mitgeben, dann sucht
PDO darin Elemente, deren Namen den Platzhaltern des SQL-Strings ohne den Doppelpunkt
entsprechen. Das ist im Import-Beispiel der Fall,
sodass Sie ganz einfach schreiben können:
$db = new PDO("sqlite:
my/path/to/database.db")
$stmt->execute(book);
Am Beispiel ist gut zu sehen, dass die PDO-Version sehr kompakt ausfällt. Das liegt unter anderem auch daran, dass wir das Ergebnis der Abfrage direkt in ein foreach()-Konstrukt einbauen
können. Das klappt, weil PDO den entsprechenden Iterator aus dem SPL-Interface unterstützt.
Möchten Sie diese verkürzte Variante nicht
nutzen, dann können Sie alternativ auch zuerst
ein Statement erzeugen, es ausführen lassen und
per fetch() jeden einzelnen Datensatz abrufen
(Listing 6).
PDO nutzt eine komfortablere Technik beim
Einsatz von Platzhaltern in Prepared Statements
als mysqli. Platzhalter werden hier nicht einfach durch ein Fragezeichen ausgedrückt, sondern erhalten einen durch einen Doppelpunkt
eingeleiteten Namen. Auf den können Sie sich
dann bei der Bindung der Parameter an konkrete Werte beziehen, was die Fehleranfälligkeit
stark verringert.
Sehen Sie sich das Beispiel der importierten
Bücherliste in Listing 7 noch einmal an, nun per
PDO gelöst.
Allerdings will der Bindungsmechanismus alle
Elemente des Arrays einem Platzhalter zuweisen. Wenn sich im Array also nicht verwen- ▶
6/2013
Bei objektrelationalen Mappern
(ORM) wie Doctrine oder Hydrogen
rückt die Datenbank ganz in den
Hintergrund (Bild 4)
117
BACKEND
LINKS ZUM THEMA
Eine tiefergehende Analyse der
Unterschiede von mysql, mysqli
und PDO in Sachen Funktionen
und Leistung
▶ http://blog.ulf-wendel.
de/2012/php-mysql-why-toupgrade-extmysql
Plädoyer zur Favorisierung von
PDO gegenüber mysqli mit anschaulichen Beispielen
▶ http://net.tutsplus.com/
tutorials/php/pdo-vs-mysqliwhich-should-you-use
dete Zusatzinformationen befinden, erhalten Sie
einen Fehler:
Invalid parameter number: number of
bound variables does not match number
of tokens
Die Platzhalter müssen also eins zu eins zu den
Array-Elementen passen.
Moderne Fehlerbehandlung
Bei der ursprünglichen MySQL-Bibliothek müssen Sie genauso wie bei mysqli aufgetretene Probleme ganz klassisch aufspüren: durch Auswertung der Rückgabewerte von Funktionen oder
Prüfung der Funktionen wie mysqli_error() auf
gespeicherte Fehlerinformationen.
PDO ist hier einen Schritt weiter und bietet an,
im Fehlerfall eine Exception auszulösen.
In der Grundeinstellung verhält sich PDO
allerdings ziemlich genauso wie mysql beziehungsweise mysqli und wirft weder Ausnahmen, noch erzeugt es PHP-Fehler. Sie müssen
das Verhalten erst durch einen Schalter aktivieren.
Die dazugehörige Einstellung, die nur ein
Exemplar aus einer ganzen Reihe von Settings
darstellt, geben Sie entweder als optionalen vierten Parameter dem Konstruktor von PDO mit,
oder Sie verwenden die Methode setAttribute():
mit die PDO-Bibliothek im Fehlerfall PHP-Warnungen erzeugt. Diese Einstellung ist nur auf
Entwicklungssystemen sinnvoll.
Ganz still ist die Grundeinstellung PDO::ERR
MODE_SILENT übrigens doch nicht: Bei fatalen Fehlern, wenn etwa bereits die DatenbankVerbindung fehlschlägt, wird doch eine Exception ausgelöst.
Ganz gleich welche Einstellung Sie wählen:
Die Fehlerursache können Sie immer dem PDOObjekt entnehmen. Dazu fragen Sie die Methode errorInfo() ab. Die liefert ein Array mit drei
Elementen: Zuerst kommt der Fehlercode nach
ANSI-SQL Standard, dann der originale Fehlercode der Datenbank und zum Schluss die Fehlernachricht des Datenbankservers.
Der Inhalt des von errorInfo() gelieferten Array sieht zum Beispiel so aus:
array(3) {
[0]=>
string(5) "42S02"
[1]=>
int(1146)
[2]=>
string(32) "Table
'mydb.bookxxx' doesn't exist"
}
Falls Sie Exceptions nutzen, erhalten Sie die Fehlermeldung natürlich wie sonst auch über die
Methode getMessage() der Exception.
$db->setAttribute(PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION);
Wie bei den php.ini-Einstellungen ist auch hier
der erste Parameter der Name des jeweiligen
Attributs und der zweite der gewünschte Wert.
Beide Informationen sind statische Konstanten
der PDO-Klasse.
Es gibt noch zwei weitere Werte für den Fehlermodus: Standard ist PDO::ERRMODE_SILENT, was keinerlei Fehler oder Exceptions erzeugt, und PDO::ERRMODE_WARNING, wo-
LISTING 7: PDO-LÖSUNG
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "INSERT INTO book( title,author_idauthor) VALUES
(:title,:author_id)";
try {
$stmt = $db->prepare($sql);
foreach ($import as $book) {
$stmt->bindParam(':title', $book['title'], PDO::PARAM_STR);
$stmt->bindParam(':author_id', $book['author_id'],
PDO::PARAM_INT);
$stmt->execute();
}
} catch (Exception $exc) {
echo "Fehler: " . $exc->getMessage() ;
}
118
Objektrelationale Mapper
Doctrine und Hydrogen
Die angesprochene Abstraktion von der konkret
verwendeten Datenbank mittels PDO bedeutet
nicht, dass die Bibliothek SQL-Befehle auf magische Weise der jeweils verwendeten Datenbank
anpasst. So etwas leisten objektrelationale Mapper (ORM) wie Doctrine oder Hydrogen, bei denen für Sie als Programmierer die Datenbank
ganz in den Hintergrund rückt (Bild 4).
Die Leistung von PDO auf diesem Gebiet besteht also vor allem darin, dass Sie nur durch
Änderungen an der DSN mit einer anderen Datenbank reden können und für alle Aktionen mit
dem Datenbankserver dieselben Methoden anwenden können.
Ob Ihre Anwendung dann mit einer anderen
Datenbank reibungslos läuft, wird davon abhängen, wie viele der SQL-Abfragen Spezialitäten nutzen, die datenbankspezifisch sind. Typische Kandidaten dafür sind ausgefeilte StringFunktionen oder exotische JOIN-Konstruktionen. Wo immer Sie können, sollten Sie davon
Abstand nehmen. Ansonsten ist später einiges
an Handarbeit notwendig, um eine Anwendung
fit für eine andere DB zu machen.
[mb]
6/2013