6.4 Integration von Database 

Während der Vorstellung der Komponente in diesem Kapitel werden unter anderem mehrere SELECT-Queries abgesetzt. Um deren Resultate besser zeigen zu können, liegt im Verzeichnis docs/ eine Datei gpblog_data.sql, die zusätzlich zur Tabellenstruktur, die Sie bereits aus Abschnitt 1.3.2, »Aufbau der Beispielanwendung«, kennen, auch Beispieldaten enthält. Zu den in diesem Kapitel vorkommenden SELECT-Statements werden Sie die Ergebnisse auf Basis dieser Daten sehen.
Der erste Schritt zur Interaktion mit einer Datenbank ist der Aufbau der benötigten Datenbankverbindung. Die Verbindung wird während der Initialisierung des Controllers aufgebaut, sodass sie im GP-Blog zur Verfügung steht, bevor es zur Ausführung von Applikationslogik kommt.
$db = ezcDbFactory::create(
$this->configuration-> getSetting( 'site', 'installation', 'dsn' )
);
ezcDbInstance::set( $db );Listing 6.4 Datenbankverbindung aufbauen
Die Datenbankverbindung definiert sich durch einen Data-Source-Name (DSN), den wir aus der Konfigurationsdatei site.ini erhalten. Die Verwendung der Konfigurationsdateien wurde im vorhergehenden Kapitel 5, »Konfiguration«, ausführlich beschrieben. Der DSN ist wie folgt aufgebaut:
rdbms://user:password@host/database
Das Schema dieses URI-Strings zeigt den von der Database-Komponente zu erzeugenden Backend-Handler an, der RDBMS-spezifisch implementiert ist. Benutzername und Passwort für die Verbindung werden, wie aus URLs gewohnt, vor dem Host durch ein @-Zeichen getrennt angegeben. Der Pfadteil, welcher lediglich eine einzige Ebene enthalten darf, spezifiziert die in der Verbindung zu benutzende Datenbank über deren Namen.
ezcDbFactory instanziiert die für das RDBMS relevante Handler-Klasse, welche vom ezcDbHandler ableitet und gibt sie zurück. Dieses Objekt kann sofort für Queries eingesetzt werden oder in der Registry ezcDbInstance zur späteren Verwendung abgelegt werden. Dies ist besonders dann sinnvoll, wenn in einer Applikation an verschiedenen Stellen mehrfach auf die gleiche Datenbankverbindung zurückgegriffen wird. Der Mechanismus ist ebenfalls in der Lage, mehrere Datenbankverbindungen zu speichern, was praktisch ist, falls Sie zu unterschiedlichen Datenbanken Verbindungen aufbauen und bereitstellen wollen.
| Das Registry-Pattern |
|
Globale Variablen sind besonders in größeren Applikationen ein wachsendes Problem, weil nicht nachvollzogen werden kann, welcher Teil der Applikation zu welchem Zeitpunkt eine Variable geändert hat. |
|
Das Registry-Pattern zeigt einen Mechanismus auf, ein zentrales Interface bereitzustellen, in dem eine Menge von Objekten über einen Namen registriert werden kann, um später in anderen Teilen der Applikation abgefragt und verwendet zu werden. |
|
Ein solcher zentraler Mechanismus kann auf der einen Seite sicherstellen, dass nur Objekte des richtigen Typs in der Registry landen, und auf der anderen Seite können bei möglichen Konflikten die Zugriffe einfach geloggt oder überprüft werden. |
|
Das Entwurfsmuster kann als eine Art erweitertes Singleton-Pattern (Abschnitt 2.3.4, »Lazy-Initialization«) angesehen werden. |
Das Registry-Pattern wird hier anstelle eines einfachen Singletons verwendet, damit es möglich ist, mehrere verschiedene Datenbankverbindungen aufzubauen und sie zentral zu verwalten, wohingegen das Singleton-Pattern verwendet wird, um genau die Existenz einer einzigen Instanz eines Objekts sicherzustellen. So kann ezcDbInstance::set() optional als zweiter Parameter ein Schlüssel übergeben werden, über den dann eine konkrete Verbindung später mit ezcDbInstance::get() abgefragt werden kann.
6.4.1 Ein neues Blog Posting erstellen 

Die verwendete MVC-Architektur wurde in Kapitel 3, »Die Applikationsbasis«, ausführlich beschrieben. Abhängig von Parametern in der URL werden sogenannte Action-Klassen aufgerufen, welche die Businesslogik implementieren. Entsprechend dieser Architektur wird zum Anlegen von neuen Blog-Einträgen der Action-Handler modifiziert, der für das Erstellen der Einträge verantwortlich ist. Die entsprechende Logik finden Sie in der Klasse gpBlogActionEntryEdit in der Datei classes/actions/entry_edit.php. Entsprechend der Dispatcher-Regeln wird die statische Methode save() aus dieser Klasse aufgerufen, die jetzt für das Absetzen eines INSERT-Statements an die Datenbank erweitert wird.
$db = ezcDbInstance::get();
$query = $db->createInsertQuery();
$query->insertInto( $db->quoteIdentifier( 'entry' ) )
->set(
$db->quoteIdentifier( 'title' ),
$query->bindValue( $_GET['title'] ) )
->set(
$db->quoteIdentifier( 'body' ),
$query->bindValue( $_GET['body'] ) )
->set(
$db->quoteIdentifier( 'date' ),
$query->bindValue( time() ) );
$query->prepare()->execute();
$id = $db->lastInsertID();Listing 6.5 Insert-Query erzeugen
In der ersten Zeile des Beispiels wird der Variablen $db, die vorher im Controller erzeugte ezcDbHandler-Instanz zugewiesen, indem sie aus der ezcDbInstance-Registry abgefragt wird. Wie bereits erwähnt, stellen die ezcDbHandler Factory-Methoden für den Query-Builder zur Verfügung, damit auch der Query-Builder RDBMS-Spezifika beachten kann. Entsprechend wird in der dritten Zeile des Listings ein neues Objekt zur Generierung von INSERT-Statements produziert.
Der vorhergegangenen Erklärung zu Fluent-Interfaces folgend, können auf dem Query-Objekt nun mehrere Methoden in Folge aufgerufen werden. Zu Beginn wird die Tabelle spezifiziert, in die der Blog-Eintrag eingefügt werden soll. Eine Beschreibung des Datenbankschemas, das für das Blog verwendet wird, findet sich in Abschnitt 1.3.2, »Aufbau der Beispielanwendung«. In den darauf folgenden drei Methoden, die auf dem Query-Objekt aufgerufen werden, werden die in die Datenbanktabelle einzufügenden Daten spezifiziert.
Der Aufruf der Methode quoteIdentifiers() auf dem Datenbank-Handler-Objekt stellt sicher, dass auch Spalten- und Tabellennamen entsprechend des RDBMS maskiert werden. Wenn diese Werte nicht maskiert werden, kommt es bei vielen RDBMS zu Problemen, falls Sie im RDBMS reservierte Begriffe verwenden wollen, da die Bedeutung des Begriffs vom Query-Parser nicht mehr eindeutig bestimmbar ist. Die Liste der reservierten Begriffe unterscheidet sich zwischen den verschiedenen RDBMS stark und kann sich zwischen verschiedenen Versionen ständig erweitern, sodass es sinnvoll ist, sämtliche Spalten- und Tabellennamen zu maskieren. Außerdem zeigen verschiedene RDBMS seltsame Verhaltensweisen, wie beispielsweise das Nicht-Beachten von Groß-Klein-Schreibung.
Da die Beschreibung der Verarbeitung von Formularen und Eingabe-Validierung mit den eZ Components erst in Kapitel 10, »Benutzereingaben validieren«, folgt, übernehmen wir an dieser Stelle die Parameter direkt aus der URL, welche PHP über das $_GET-Array zur Verfügung stellt. Falls der Zugriff auf diese Variablen beim Testen der Applikation auf der Buch-CD Probleme bereitet, beachten Sie den Kasten zum Thema variables_order. Als Datum des Blog-Eintrags wird die aktuelle Zeit als Unix-Zeitstempel gespeichert.
Wenn Sie das Blog, wie in Abschnitt 1.3.3, »Testen des GP-Blogs«, beschrieben installiert haben, kann das Einfügen eines neuen Blog-Eintrags über die URL http://gpblog/save_entry/?title=Neuer%20Eintrag&body=Inhalt getestet werden. Im Browser sollten Sie als Resultat die folgende Ausgabe zu sehen bekommen, die die Bestätigung für das Einfügen zeigt:
Abbildung 6.1 Anlegen eines neuen Blog-Eintrags
| variables_order |
|
Seitdem die filter-Erweiterung, die zusammen mit der ezcUserInput in Kapitel 10, »Benutzereingaben validieren«, vorgestellt wird, Einzug in PHP 5.1 gehalten hat, wird in manchen PHP-Installationen die php.ini-Einstellung variables_order leer gelassen, damit der Zugriff auf Request-Variablen nur noch über ext/filter stattfinden kann. In diesem Fall sind die Arrays $_POST, $_GET etc. nicht definiert, und der Zugriff auf diese resultiert in einer Fehlermeldung mit dem Level E_NOTICE. Sie können das Problem einfach durch folgende Konfigurationsänderung in Ihrer php.ini-Datei beheben: |
variables_order = "EGPCS" |
bindValue() und prepare() verwenden Sie, um dem SQL-Statement Daten zum Einfügen anzuhängen. Im folgenden Abschnitt zu Prepared-Statements gehen wir noch genauer auf den Grund dafür ein. Mit execute() wird das so erzeugte Statement ausgeführt, und das Resultat steht der Applikation zur Verfügung. Im Falle eines INSERT-Statements kann das Resultat ein Fehler sein, wobei eine Exception geworfen würde oder ein erfolgreiches Einfügen des Datensatzes in die Datenbank.
Wie vorher erwähnt, leitet der ezcDbHandler und damit auch die RDBMS-spezifischen Subklassen von der PDO-Klasse ab. Damit stehen deren Methoden wie lastInsertId() auch über ezcDbHandler-Implementierungen zur Verfügung. Die ID des eingefügten Datensatzes wird in der Action-Klasse verwendet, um direkt zum frisch erstellen Blog-Eintrag weiterzuleiten.
6.4.2 Prepared-Statements 

Vorbereitete SQL-Statements (Prepared-Statements) bezeichnen einen Mechanismus, mit dem Sie eine Query ohne Daten vorbereiten können und die Daten erst später einfügen. Dieser Mechanismus bietet mehrere Vorteile:
| 1. | Sicherheit Das RDBMS weiß, welcher Teil des Statements Daten sind und welcher Teil ein SQL-Kommando darstellt. Dadurch wird nicht versucht, die Daten als SQL zu interpretieren, was SQL-Injektion unmöglich macht. |
| 2. | Geschwindigkeit Das Parsen eines Statements durch das RDBMS wird wesentlich beschleunigt, da die Query lediglich ohne enthaltene Daten geparst werden muss, die besonders bei INSERT- und UPDATE-Statements häufig den größten Teil einer Abfrage ausmachen. Hinzu kommt, dass Sie ein Statement mit verschiedenen Datensätzen wiederverwenden können, in dem nur neue Daten an die Abfrage gebunden werden. Dies vermeidet das wiederholte Parsen der gleichen Abfrage, was ebenfalls, besonders für viele INSERT- und UPDATE-Statements in Folge, entscheidend sein kann. |
| SQL-Injektion |
|
SQL-Injektion bezeichnet eine beliebte Attacke gegen Anwendungen, die auf einer Datenbank aufbauen, die mit SQL angesprochen wird und Eingaben von Benutzern der Applikation in dieser Datenbank speichert. |
|
Bei der Attacke werden die Eingaben so verändert, dass über die ursprüngliche Abfrage hinaus potentiell vertrauliche Daten angezeigt oder Daten in der Datenbank verändert werden, auf die der Benutzer eigentlich keinen Zugriff haben sollte. |
Mit der Methode bindValue() wird im obigen Beispiel diese Bindung von Daten an die erzeugte Query ausgeführt. Aufgrund des vorbereiteten Statements müssen Sie sich keine Sorgen um Manipulationen der Abfrage durch den Benutzer machen, obwohl man Benutzereingaben ungeprüft in der Abfrage verwendet. Ohne Prepared-Statements sollten Sie dies niemals tun!
Auf eine logische Überprüfung der Eingaben verzichten wir in diesem Beispiel, da es den Rahmen der Beschreibung von Database sprengen würde.
Die Methode prepare() bereitet das Statement auf die Ausführung mittels execute() vor. Falls das verwendete RDBMS keine Prepared-Statements unterstützt, wird die Abfrage in diesem Schritt entsprechend zusammengebaut, um das Verhalten zu simulieren. PDO stellt auch in diesem Fall sicher, dass die Daten, welche an die Datenbank übergeben werden, korrekt maskiert werden. Wie ein späteres Beispiel zeigen wird, ist es möglich, bis zu diesem Zeitpunkt Platzhalter für die Daten in der Query zu definieren, die erst beim Ausführen der Abfrage mittels execute() gefüllt werden.
6.4.3 Update 

Die save()-Methode aus gpBlogActionEntryEdit soll nun dahingehend modifiziert werden, dass, wenn eine ID in der URL übergeben worden ist, ein bestehender Blog-Eintrag aktualisiert wird, anstatt erneut einen neuen Blog-Eintrag anzulegen. Dazu wird folgende Logik implementiert:
if ( ( $id = $url->getParam( "id" ) ) === null )
{
// Insert
}
else
{
// Update
}Listing 6.6 Unterscheidungslogik
Falls es möglich ist, aus der in Kapitel 3, »Die Applikationsbasis«, beschriebenen Url-Komponente einen ID-Parameter zu extrahieren, wird das UPDATE-Statement ausgeführt, im anderen Fall das zuvor beschriebene INSERT-Statement. Das UPDATE-Statement unterscheidet sich vom INSERT-Statement, dadurch, dass zusätzlich ein Einschränkung mittels WHERE-Klausel angegeben werden muss, um zu beschreiben, welcher Datensatz aktualisiert werden soll.
$query = $db->createUpdateQuery();
$query->update( $db->quoteIdentifier( 'entry' ) )
->set(
$db->quoteIdentifier( 'title' ),
$query->bindValue( $_GET['title'] )
)
->set(
$db->quoteIdentifier( 'body' ),
$query->bindValue( $_GET['body'] )
)
->where(
$query->expr->eq(
$db->quoteIdentifier( 'id' ),
$query->bindValue( $id )
)
)
);
$query->prepare()->execute();Listing 6.7 Update-Query erzeugen
In der ersten Zeile des Listings wird eine neue UPDATE-Query über die createUpdateQuery()-Factory-Methode erzeugt. Auf dem Query-Objekt wird durch die Methode update() spezifiziert, welche Tabelle aktualisiert werden soll. Mit der Methode set() werden, genau wie beim INSERT, den Spaltennamen der Tabelle die zu aktualisierenden Werte zugewiesen. Das Datum aktualisieren wir mit dieser Query nicht, da es weiterhin das Datum der Erstellung des Blog-Eintrags reflektieren soll.
Die Bedingung der Query wird über die Methode where(), dem Äquivalent zum WHERE in SQL, erzeugt. In dieser Query können wir die Identität des Datensatzes vollständig über die Gleichheit des Werts in der Spalte id mit der übergebenen ID sicherstellen. Ansonsten sind auch komplexere Bedingungen mit dem Query-Builder möglich, wie ein späteres Beispiel zeigen wird.
Zum Erzeugen der Bedingungen findet sich in $query->expr ein Objekt der Klasse ezcQueryExpression, das Ihnen verschiedene Methoden für die in SQL möglichen logischen Konstrukte bereitstellt. In diesem Fall wollen wir SQL generieren, welches die Spalte id auf Gleichheit (equality wird zu eq()) mit dem übergebenen Wert überprüft. Um SQL-Injektion vorzubeugen, wird der übergebene Wert erneut per bindValue() an das Statement gebunden.
Anschließend wird, wie bei der obigen INSERT-Query, das Statement vorbereitet und ausgeführt. Ähnlich wie im vorigen Beispiel kann diese Query auch im Browser getestet werden, und zwar über die folgende URL: http://gpblog/save_entry/4/?title=Neuer%20Eintrag&body=Modifizierter%20Inhalt
Die Ausgabe ist erneut eine Bestätigung, dass die Änderung erfolgreich war.
Abbildung 6.2 Der Eintrag wurde erfolgreich geändert.
6.4.4 Daten auslesen 

Nachdem Sie nun gesehen haben, wie die ersten einfachen Blog-Einträge erstellt worden sind, sollen diese natürlich auch angezeigt werden. In diesem Abschnitt implementieren wir lediglich die Methode, um einen Blog-Eintrag gezielt anzuzeigen, da das folgende Kapitel die direkte Benutzung der Database-Komponente überflüssig macht. Um eine Liste aller Blog-Einträge anzuzeigen, müssen Sie lediglich die Einschränkung mittels where() entfernen und das Template anpassen.
Die Anzeige des Blog-Eintrags wird in gpBlogActionEntryDisplay::showEntry() in der Datei classes/actions/entry_display.php implementiert. Da ein konkreter Blog-Eintrag angezeigt werden soll, muss zu Beginn die ID des anzuzeigenden Eintrags aus der URL extrahiert werden.
if ( ( $id = $url->getParam( "id" ) ) === null )
{
gpBlogController::getInstance()->mainSignals->emit(
"error",
"No valid entry ID provided. The link you've been
following seems to be broken."
);
return;
}Die Abfrage erklärt sich aus dem dritten Kapitel, in dem die Verwendung der Url-Komponente und des Signal-Slot-Mechanismus erklärt wurden. Falls keine ID in der URL gefunden werden konnte, wird ein Fehler ausgelöst und die Ausführung der Methode mittels return; beendet.
$db = ezcDbInstance::get();
$query = $db->createSelectQuery();
$query
->select(
$db->quoteIdentifier( 'title' ),
$db->quoteIdentifier( 'body' ),
$db->quoteIdentifier( 'date' )
)
->from( $db->quoteIdentifier( 'entry' ) )
->where(
$query->expr->eq(
$db->quoteIdentifier( 'id' ),
$query->bindValue( $id )
)
);
$statement = $query->prepare();
$statement->execute();Listing 6.8 Select-Query erzeugen
Wie in dem vorangegangenen Beispiel wird zunächst eine Instanz des ezcDbHandlers benötigt, der die Verbindung zur Datenbank repräsentiert. Aus dem DB-Handler wird ein Objekt erfragt, das hilft, eine SELECT-Query aufzubauen. Die Spalten, die ausgelesen werden sollen, lassen sich entweder mittels beliebig vieler Parameter bei einem Aufruf von select() übergeben, als Array von Spaltennamen oder durch mehrfache Aufrufe der Methode select() mit jeweils einem Parameter. Danach spezifiziert from() die Tabelle, aus der die Daten abgefragt werden, wobei Sie auch diesen Aufruf mehrfach ausführen können, um aus mehreren Tabellen zu selektieren. Die Einschränkung über where() ist aus dem UPDATE-Beispiel bekannt.
Die Reihenfolge der Methoden ist irrelevant, doch lässt sich eine mit dem Query-Builder generierte Query durch jeden Entwickler, der SQL gewohnt ist, einfach lesen und verstehen, wenn Sie die Methoden-Aufrufe ähnlich gewöhnlichem SQL strukturieren und formatieren.
Was in den obigen Beispiel nicht relevant war, zeigt dieser Codeabschnitt besser. $query->prepare() gibt ein Objekt der Klasse PDOStatement zurück, auf dem dann die PDOStatement-Methode execute() aufgerufen wird. Aus diesem Grund ist der Zugriff auf das Ergebnis der Abfrage der gleiche, wie Sie es aus PDO gewohnt sind.
$result = $statement->fetchAll(); $result = reset( $result ); +--------------------+----------------------------+------------+ | title | body | date | +--------------------+----------------------------+------------+ | Erster Blogeintrag | Willkommen im Galileo | 1178292180 | | | Press Blog. Dies ist ein | | | | erster Testeintrag. | | +--------------------+----------------------------+------------+
Listing 6.9 Resultat der Select-Query
Aufgrund der Abfrage über den Primärschlüssel der Tabelle wissen Sie, dass maximal ein Ergebnis zurückgegeben wird. Die Methode fetchAll() liefert ein Array mit allen Ergebnissen der Abfrage zurück und gibt danach den Speicher und die Verbindung für neue Abfragen frei. Bei großen Ergebnismengen kann ein solches Array mit allen Resultaten sehr viel Speicher auf einen Schlag belegen. Sie sollten daher besser über das Ergebnis iterieren. In diesem Fall ist es jedoch ausreichend, $result durch einen Aufruf von reset() das erste Element des Arrays zuzuweisen und dieses anschließend an das view-Signal des GP-Blogs weiterzureichen.
Im Browser kann das Ergebnis der Abfrage über die URL http://gpblog/show_entry/1 angezeigt werden.
Abbildung 6.3 Ein einzelner Eintrag wird ausgelesen und dargestellt.







Ihre Meinung






