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