Ein Quantum Daten

Eine der großen Stärken von PHP ist die Möglichkeit, einfach auf Datenbanken zuzugreifen. Zudem setzt sich objektorientierte Programmierung bei den Anhängern dieser Sprache allmählich durch. Doctrine ist der Versuch, ein objektrelationales Mapping-Framework in PHP auf die Beine zu stellen, das den Vergleich zur Konkurrenz nicht zu scheuen braucht.

vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 9 Min.
Von
  • Timo Haberkern
Inhaltsverzeichnis

Nach langer Anlaufzeit hat sich die objektorientierte Programmierung in der PHP-Welt durchgesetzt. Daher schlagen sich PHP-Entwickler mittlerweile mit denselben Schwierigkeiten herum, die zuvor Anhänger anderer Programmiersprachen schon hatten: die in relationalen DBMSen vorliegenden Daten mit der Objektorientierung zu „verheiraten“. Letztlich verwundert es kaum, dass aus diesem Grund der Bedarf an ähnlichen Hilfsmitteln entstanden ist, etwa Frameworks für objektrelationales Mapping (ORM) – der automatischen Umsetzung von Objektdaten in relationale Datenbanken.

Mit Doctrine liegt eine solche ORM-Bibliothek vor. Sie orientiert sich am Hibernate-Framework der Java-Welt und nimmt viele Anleihen bei diesem. Nach langer Entwicklungszeit erblickte vor Kurzem die stabile Version 1.0 von Doctrine das Licht der Programmierwelt. Das Framework hat seine Wurzeln im Jahr 2006, als Konsta Vesterinen das Framework im Rahmen des Google Summer of Code entwickelte. Mittlerweile arbeitet allerdings ein Kernteam von drei Entwicklern an der kontinuierlichen Weiterentwicklung. Doctrine steht unter der LGPL, ist demnach kostenlos erhältlich und setzt mindestens PHP 5.2.3 voraus. Momentan liegt schon eine Alpha-Version 1.1 vor.

Vom Zugriff bis zum Mapping: die drei Schichten des Doctrine-Frameworks (Abb. 1)

Abbildung 1 zeigt die drei Hauptschichten des Framework. Der eigentliche Datenbankzugriff erfolgt auf Basis von PDOs (PHP Data Objects), die seit der Version 5.1 ein fester Bestandteil der Skriptsprache sind. Dadurch kann Doctrine von Haus aus mit den wichtigen DBMSen umgehen. Dazu gehören neben MySQL und PostgreSQL unter anderem Oracle, SQLite, Informix und Microsofts SQL Server. PDO bietet zwar eine einheitliche Schnittstelle für den Zugriff, stellt aber keine komplette Datenbankabstraktion zur Verfügung. Genau dies erledigt die zweite Schicht von Doctrine. In diesem Layer baut das Framework SQL-Statements zusammen, die dem Standard des verwendeten DBMS entsprechen. Dies erlaubt es, es zu einem späteren Zeitpunkt zu wechseln, ohne dass Änderungen an der Anwendung notwendig sind. So werden datenbankspezifische Besonderheiten wie die Behandlung von Primärschlüsseln oder die Einschränkung der Ergebnismenge von der Anwendungslogik abstrahiert. Doctrine kümmert sich selbstständig um die korrekte Umsetzung in der für ein Projekt verwendeten Datenbanksoftware.

Die letzte Schicht ist zugleich die interessanteste, denn in ihr sind alle Funktionen untergebracht, die das Mapping von Objektdaten in relationale Strukturen ermöglichen (siehe unten).

Frameworks wie Doctrine übernehmen die unangenehme Arbeit der Umsetzung objektorientierter Daten in relationale Strukturen. Sie ersparen Entwicklern die Arbeit, Datenbankzugriffsklassen zu programmieren. Damit Doctrine diese Aufgabe übernehmen kann, muss selbstverständlich irgendwo definiert sein, wie eine solche Zuordnung auszusehen hat. Prinzipiell kennt das Framework zwei Wege zur Mapping-Definition. Die einfachere ist die Beschreibung der Objekte und Datenbanktabellen innerhalb einer Konfigurationsdatei. Diese sogenannte Schema-Definition muss im YAML-Format (rekursives Akronym: YAML Ain’t Markup Language) vorliegen, das unter anderem in Frameworks wie Ruby on Rails oder Symfony zum Einsatz kommt. Listing 1 zeigt eine Schema-Definition, die zwei Klassen mit unterschiedlichen Properties definiert. Beide Klassen sind durch eine 1:n-Beziehung miteinander verbunden.

Mehr Infos

Listing 1: Zwei Schema-Definitionen

Author:
columns:
id: { type: integer, primary: true, autoincrement: true }
contact_id: { type: integer(4) }
first_name: { type: string(255) }
last_name: { type: string(255) }
Book:
columns:
id: { type: integer, primary: true, autoincrement: true }
name: { type: string(255) }
user_id: { type: integer(4) }
relations:
Author:
foreignAlias: Books
Mehr Infos

Auf diese Weise werden alle speicherbaren Objekte definiert. Das Listing enthält die beiden Objekte/Tabellen Author und Book mit der Beschreibung der zugeordneten Eigenschaften. Für jede Eigenschaft setzt es verschiedene Attribute in geschweiften Klammern, hier hauptsächlich Typ und Länge der Tabellenfelder.

Zum Einrichten von Relationen zwischen den einzelnen Klassen und Tabellen stehen mehr Möglichkeiten zur Verfügung, als dies beim ORM-Konkurrenten Propel der Fall ist (siehe [1] und den Kastentext „Alternativen zu Doctrine“). So unterstützt Doctrine die Many-To-Many-Beziehung und erleichtert damit den Zugriff auf solche Elemente. Diese Art der Beziehung ist auf Objektebene ganz alltäglich, auf Datenbankebene muss man sie über Join-Tabellen auflösen, was Doctrine übernimmt.

Mehr Infos

Alternativen zu Doctrine

Mit Propel [1] existiert schon seit einiger Zeit ein ausgereiftes ORM-Framework für PHP. Es orientiert sich stark an der Java-Lösung Torque [c] aus dem Apache-Feld. Allerdings schreitet die Entwicklung von Propel eher gemächlich voran, und die Funktionen decken nur einen kleinen Teil derer von Doctrine ab. Vor Kurzem wurde allerdings die lang erwartete Version 1.3 freigegeben, die wie Doctrine auf PDO als Datenbankzugriffsschicht zurückgreift. Das hat die Performance stark verbessert; sie liegt über der von Doctrine. Wer also auf die vielen Funktionen und den hohen Komfort von Doctrine verzichten kann, der sollte eventuell einen Blick auf Propel werfen.

Andere ORM-Frameworks für PHP arbeiten noch lange nicht auf dem Niveau von Doctrine und Propel oder sind fest in MVC-Frameworks integriert und lassen sich nicht aus diesen herauslösen.

So weit, so gut. Dafür, wie man diese Schema-Beschreibung konkret nutzt, liefert die aktuelle Version von Doctrine leider nur wenig Hilfestellung, und man muss noch über ein kleines Hilfsskript ausweichen, dessen Aufbau die Dokumentation detailliert beschreibt [e]. Eine der nächsten Versionen soll ein Kommandozeilen-Tool für diese Aufgabe enthalten. Im Moment muss man sich aber noch mit dem Aufruf des Hilfsskripts begnügen:

$> php doctrine.php build-all

Das objektrelationale Mapping-Framework erzeugt Datenbank-Zugriffsklassen (Abb. 2)

Zum einen erzeugt dieser Aufruf Klassen, die abhängig von der Schema-Definition auf die Datenbank zugreifen. Zum anderen generiert er eine Textdatei, die SQL-Befehle enthält, die die Datenbankstruktur anlegen können. Abbildung 2 zeigt das prinzipielle Vorgehen nochmals in grafischer Form.

Wer eine der generierten Dateien öffnet, sieht den zuvor angedeuteten zweiten Weg der Mapping-Definition. Man kann das Mapping direkt in Klassen implementieren, wie es die soeben erzeugten Klassen tun. Dieser Weg ist zwar mit mehr Mühe verbunden, hat aber den Vorteil, dass Änderungen an den Klassen mitversioniert werden, sofern man ein System wie Subversion verwendet.

Mit den aus den Mapping-Informationen generierten Klassen lassen sich Datenbankzugriffe einfach durchführen. Für alltägliche Operationen sind keine SQL-Befehle mehr notwendig. Das folgende Kurzlisting zeigt das Anlegen eines neuen Artikels und das darauf folgende Speichern in der Datenbank. Der Primärschlüssel, in diesem Fall das Feld id, wird nach dem Speichern automatisch mit dem entsprechenden Schlüsselwert aus der Datenbank gefüllt.

<?php
$value = new Article();
$value->name = "V21A Kollektor";
$value->article_number = "164304";
$value->save();
echo "ID=".$value->id;
?>

Ähnlich einfach gestaltet sich das Lesen von Daten mit einem bekannten Schlüsselfeld. Das geladene Objekt lässt sich anschließend verändern, speichern oder löschen.

<?php
// Lesen von Werten
$value = Doctrine::getTable("Article")->find($id);
// Verändern
$value->name = "Geänderter Wert";
$value->save();
// Löschen
$value->delete();
?>

Mit den gezeigten Mitteln lassen sich einfache Abfragen realisieren. Selbstverständlich genügt dies nur grundlegenden Anforderungen. Nicht umsonst ist SQL so flexibel und dadurch so mächtig. Doctrine bringt seine eigene, an SQL angelehnte Anfragesprache (Doctrine Query Language, DQL) mit, die auf Objektebene arbeitet, wie an Listing 2 zu sehen ist.

Mehr Infos

Listing 2: Einfache DQL-Beispiele

<?php
// Einfacher Lesender Zugriff
$query = new Doctrine_Query();
$query->from('Author a')->where("a.LastName = 'Haberkern'");
$result = $query->execute();

// Lesender Zugriff mit Joins
$query->from('Author a')
->leftJoin('a.Books b')
->innerJoin('a.Publisher p WITH a.id > 3')
$users = $query->execute();

// Lesender Zugriff mit Prepared Statements
$query->from('Author a')->where('a.id = ? AND
a.LastName LIKE ?', array(1, '%Haber%');

Bei Ausführung eines solchen DQL-Statements beginnt im Hintergrund die große Arbeit. Doctrine setzt dieses Statement in einen für das jeweilig verwendete Datenbanksystem gültigen SQL-Befehl um. Der Clou der Sache ist die Tatsache, dass man den genauen Aufbau der Datenbanktabellen nicht kennen muss. Dieser darf sich sogar ändern, ohne dass eine Anpassung des Programms notwendig wäre. Die DQL besitzt selbstverständlich mehr Funktionen als Listing 2 zeigen kann. Joins sind ebenso machbar wie Transaktionen. Wem dies alles nicht genügt oder wer ganz spezielle Kommandos an die Datenbank senden möchte, der kann native SQL-Statements über Doctrine absetzen. Dies kann beispielsweise bei der Verwendung von Stored Procedures notwendig werden.

Vererbung und Polymorphismus sind Konzepte der objektorientierten Programmierung, die in fast jeder Anwendung zum Einsatz kommen. Relationale Datenbanken beherrschen allerdings beide Konzepte nicht. Entwickler müssen sie daher manuell nachbilden. Doctrine erleichtert hier die Arbeit, indem es diese Nachbildung schon integriert hat. Vererbungsbeziehungen zwischen Klassen löst Doctrine automatisch auf und bildet sie in relationalen Strukturen ab. Hierzu stehen drei unterschiedliche Strategien zur Verfügung:

  • Simple Inheritance ist die einfachste der verfĂĽgbaren Strategien. Hier teilen sich die Basisklasse und alle davon abgeleiteten Klassen eine Tabelle in der Datenbank. Dies ist aus Performance-Sicht zwar optimal, aus Datenbank-Design-Sicht allerdings der blanke Horror.
  • Class Table Inheritance: Bei dieser Form der Umsetzung wird pro Klasse eine separate Tabelle in der Datenbank vorgesehen.
  • Column Aggregation Inheritance: Bei dieser Strategie wird ebenfalls fĂĽr jede Klasse eine Tabelle angelegt. Allerdings existiert auĂźerdem eine Tabelle fĂĽr die Daten der Basisklasse. Einen Zugriff auf die Daten eines Objekts setzen Joins in der Datenbank um.

Welche Strategie zum Einsatz kommt, entscheidet die Definition in der Mapping-Beschreibung. Listing 3 zeigt die Definition einer Vererbungsbeziehung zwischen zwei Klassen, als Strategie kommt Column Aggregation Inheritance zum Einsatz.

Mehr Infos

Listing 3: Vererbungsbeziehung

Entity:
columns:
name: string(30)
username: string(20)
password: string(16)
created: integer(11)

User:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 1

Group:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 2

Mit Doctrine existiert ein ORM-Framework, das den Vergleich mit Lösungen anderer Plattformen nicht zu scheuen braucht. Die in diesem Artikel gezeigten Listings beleuchten nur einen Bruchteil der Funktionen des Framework. Seit sich ein festes Entwickler-Team gefunden hat, schreitet die Entwicklung kontinuierlich voran, die Dokumentation ist hervorragend, und die Community wächst und bietet eine gute Anlaufstelle.

Die mächtigen Funktionen von Doctrine haben allerdings ihren Preis. Sowohl die Geschwindigkeit als auch der Speicherverbrauch liegen weit von dem entfernt, was mit einem direkten Datenbankzugriff über die PHP-Bordmittel erreichbar wäre. Den zusätzlichen Komfort und die erhöhte Produktivität erkauft man sich deshalb zu einem hohen Preis. Wer Hochlastanwendungen entwickelt, dürfte daher wohl von einer ORM-Lösung absehen.

[1] Timo Haberkern; PHP-Programmierung; BrĂĽcken schlagen; Objektrelationales Mapping fĂĽr PHP mit Propel; iX 12/2006, S. 157

iX-Links

Mehr Infos

iX-WERTUNG

[+] funktional bestes ORM-Framework fĂĽr PHP
[+] UnterstĂĽtzung von Vererbungsbeziehungen
[+] komfortable Anfragesprache (DQL)
[–] hoher Ressourcen-Verbrauch

(hb)