Objektrelationales Mapping mit FLOW3/Doctrine

Objektrelationales Mapping mit FLOW3 und Doctrine

Nachdem ich bereits in einem meiner letzten Blog-Artikel eine kleine Einführung in das Arbeiten mit FLOW3 gegeben habe, möchte ich in diesem Artikel speziell die Datenbank-Anbindung von FLOW3 betrachten. Es soll insbesondere aufgezeigt werden, wie FLOW3 Daten aus der Datenbank lädt und wieder speichert. Bereits seit einiger Zeit setzt FLOW3 dabei auf bewährte Technik und verwendet das Framework Doctrine 2 zur Datenbank-Abstraktion und zur objektrelationalen Abbildung (engl. object-relational mapping oder kurz ORM).

  1. Von Datensätzen auf Objekte und zurück: Die Aufgaben eines OR-Mappers
  2. Einfache Tabellen mit Doctrine und FLOW3 erstellen
  3. Beziehungen zwischen Objekten modellieren
  4. Fortgeschrittene Funktionalitäten von Doctrine
  5. Ausblick und weitere Informationsquellen

Von Datensätzen auf Objekte und zurück: Die Aufgaben eines OR-Mappers

Wie bereits zuvor betrachtet, erfolgt die Datenmodellierung in FLOW3 auf objektorientierter Ebene. Dies bedeutet, dass am Anfang des Datenentwurfs nicht etwa – wie häufig üblich – eine Datenbank-Tabelle steht, sondern bereits eine Klasse, die neben Attributen auch bereits Methoden enthalten und mit anderen Klassen in nahezu beliebigen Beziehungen stehen kann. Das Datenbankmodell wird dann erst im nächsten Schritt aus diesem Objektmodell abgeleitet – und das im Idealfall vollkommen automatisch. Hier kommen nun OR-Mapper wie Doctrine ins Spiel.

Grundsätzlich erfüllt ein OR-Mapper in einem Anwendungssystem folgende Aufgaben:

  • Er erstellt (mehr oder weniger automatisch oder auf Grundlage einer speziellen Konfiguration) aus einem objektorientierten Modell ein Datenbankschema, in welchem Objekte der entsprechenden Klassen gespeichert werden können. Im einfachsten Fall wird dabei je eine Klasse auf eine Tabelle und je ein Attribut auf eine Tabellenspalte abgebildet.
  • Neben der Abbildung des Schemas selbst muss der OR-Mapper auch Instanzen der jeweiligen Klassen auf Datensätze in der Datenbank abbilden können. Wird innerhalb des Anwendungssysteme ein Objekt geändert, muss auch die Repräsentation dieses Objekts in der Datenbank geändert werden.
  • Diese Abbildung muss natürlich in beide Richtungen funktionieren: So muss der OR-Mapper auch in der Lage sein, aus Datenbankzeilen wieder Instanzen der Klassen aus dem objektorientierten Modell zu erstellen.

Viele aktuelle OR-Mapper übernehmen zudem neben dem ORM selbst auch eine andere wichtige Aufgabe: die Datenbank-Abstraktion. Über eine Datenbankabstraktionsschicht können die „Eigenheiten“ des jeweiligen Datenbanksystems vor der Anwendung verborgen werden (zahlreiche Datenbanksysteme, wie etwa MySQL, PostgreSQL oder Oracle, sprechen zum Teil sehr unterschiedliche „SQL-Dialekte“, die nicht miteinander kompatibel sind). Durch die Verwendung einer Datenbankabstraktionsschicht (engl. database abstraction layer oder kurz DBAL) bleibt die Anwendung selbst unabhängig von Datenbanksystem; dadurch kann dieses beispielsweise einfach ausgetauscht werden. Unangenehme „Lock-In“-Effekte können auf diese Weise effizient vermieden werden.

Einfache Tabellen mit Doctrine und FLOW3 erstellen

Im einfachsten Fall erstellen Doctrine und FLOW3 die Datenbanktabellen ohne jedes weitere Zutun. Im letzten Tutorial verwendete ich eine Konzertagentur als Beispiel, welche ich hier der Konsistenz halber wieder aufgreifen möchte. Das entsprechende Domänenmodell enthält hier Künstler; die zugehörige PHP-Klasse könnte (vereinfacht) aussehen wie in dem unten stehenden Beispiel:

Über einen Kommandozeilen-Befehl kann FLOW3 nun angewiesen werden, das Datenbankschema aus den Domain-Model-Klassen zu erstellen oder gegebenenfalls zu aktualisieren:

Aus der obenstehenden PHP-Klasse erstellt Doctrine nun automatisch eine Tabelle mw_concerts_domain_model_artist mit den folgenden Spalten:

An der erstellten Tabelle wird deutlich, dass Doctrine jedes Klassenattribut auf eine Tabellenspalte abbildet. Der Typ der Tabellenspalte wird automatisch aus der @var-Annotation des jeweiligen Klassenattributs ermittelt (in diesem Fall etwa erkennbar an der established-Spalte, in welcher Doctrine den PHP-Datentyp \DateTime automatisch auf den entsprechenden MySQL-Datentyp abgebildet hat). Da sich darüber hinaus keines der Attribute als Identitätsattribut eignet, hat Doctrine außerdem zusätzlich die Spalte flow3_persistence_identifier angelegt, welche als Identitätsattribut und als Primärschlüssel der Datenbanktabelle dient. Ihr könnt auch eigene Attribute explizit als Identitätsattribut definieren; doch dazu später mehr.

Beziehungen zwischen Objekten modellieren

Seine wahre Stärke spielt Doctrine jedoch erst aus, wenn das Objektmodell mehrere Klassen enthält, die zueinander in Beziehungen stehen. Wie man solche Beziehungen (oder im OOP-Jargon Assoziationen genannt) modelliert, hängt von deren Multiplizität ab: grundsätzlich kann hier zwischen 1:1-, n:1- und m:n-Beziehungen unterschieden werden (zuzüglich einiger Spezialfälle, aber so soll es erstmal reichen).

1:1- und n:1-Beziehungen

Bei einer 1:1- oder n:1-Beziehung ist einem Objekt jeweils genau ein anderes Objekt zugeordnet. Beispielsweise könnte das Beispiel-Domänenmodell zusätzlich zur Klasse Artist noch eine Klasse Genre enthalten. Jeder Künstler könnte dann genau einem Genre zugeordnet werden:

Um diese Assoziation zu modellieren, reicht es nun aus, der Klasse ein entsprechendes Attribut zuzuweisen, und in der @var-Annotation den Typ der Ziel-Klasse anzugeben. Weiterhin muss Doctrine über eine spezielle Annotation mitgeteilt werden, welche Multiplizität die Beziehung hat. In diesem Fall handelt es sich um eine n:1- oder ManyToOne-Beziehung, da jeder Künstler genau einem Genre zugeordnet ist, aber einem Genre mehrere Künstler zugeordnet sein können:

Nach einem erneuten ./flow3 doctrine:update legt Doctrine nun die Tabelle mw_concerts_domain_model_genre an und fügt der Tabelle mw_concerts_domain_model_artist eine Spalte genre hinzu; diese verweist über eine Fremdschlüsselbeziehung auf die entsprechende Tabelle.

Die so entstandene Beziehung ist derzeit noch unidirektional. Zwar kann anhand eines Künstlers das Genre ermittelt werden, jedoch kann nicht anhand eines Genres auf alle Künstler dieses Genres zugegriffen werden. Um die Beziehung bidirektional zu machen, kann der Klasse Genre nun ebenfalls ein entsprechendes Attribut hinzugefügt und als 1:n- oder OneToMany-Beziehung ausgewiesen werden. Jede n:1-Beziehung ist gleichzeitig auch eine 1:n-Beziehung.

Vor allem fällt auf, dass in der @var-Annotation nun nicht mehr (nur) der Klassenname steht, sondern eine Collection – also eine Sammlung mehrerer Objekte. Diese Sammlung verhält sich später genau wie ein Array. Über den Parameter mappedBy, welcher in der Annotation angegeben wird, wird Doctrine das Attribut in der assoziierten Klasse angegeben, welches die andere Seite der bidirektionalen Beziehung abbildet. In der Klasse Artist kann die @ORM\ManyToOne-Annotation nun noch um den Parameter inversedBy="artists" erweitert werden.

m:n-Beziehungen

Bei einer m:n– oder ManyToMany-Beziehung können zwei Klassen in beliebiger Anzahl zueinander in Beziehung stehen. So könnte beispielsweise jeder Künstler auf beliebig vielen Konzerten spielen und umgekehrt auf einem Konzert beliebig viele Künstler. In Doctrine kann eine m:n-Relation genauso realisiert werden wie eine 1:n-Beziehung, nur dass hier die Annotation @ORM\ManyToMany verwendet wird:

Da eine solche m:n-Beziehung in einer relationalen Datenbank nicht abgebildet werden kann, löst Doctrine diese in zwei einzelne 1:n-Beziehungen auf und erstellt zusätzlich zur Tabelle mw_concerts_domain_model_concert eine Verknüpfungstabellemw_concerts_domain_model_concert_artists_join, welche über Fremdschlüssel mit den concert– und artist-Tabellen verknüpft ist.

Fortgeschrittene Funktionalitäten von Doctrine

Abbildung von Vererbungshierarchien

doctrine vererbungshierarchien

In objektorientierten Modellen wird häufig mit voneinander erbenden Klassen gearbeitet. Im vorliegenden Beispiel könnte das Domänenmodell etwa eine Klasse AbstractArtist enthalten, und zwei Klassen Musician und Comedian, die von dieser Klasse erben und gegebenenfalls um weitere Attribute und Methoden erweitern. Grundsätzlich kennt Doctrine drei verschiedene Möglichkeiten, solche Vererbungshierarchien abzubilden:

  • Standardmäßig verwendet Doctrine die sogenannte Concrete Table Inheritance. Hierbei wird jede konkrete (nicht-abstrakte) Klasse auf eine eigene Tabelle abgebildet.
  • Bei der Single Table Inheritance werden alle Abkömmlinge der abstrakten Klasse in einer einzelnen Tabelle gespeichert. Diese Tabelle enthält Spalten für die Attribute aller möglichen erbenden Klassen und eine zusätzliche Spalte, in der die konkrete Klasse des Objekts gespeichert wird.
  • Bei der Class Table Inheritance existiert eine Tabelle für die abstrakte Klasse, sowie einzelne Tabellen für jede konkrete Klasse. Bei Abfragen wird die Basistabelle mit der entsprechenden konkreten Tabelle per JOIN-Statement verknüpft.

Welche dieser Strategien Doctrine nun wirklich verwenden soll, kann über eine Annotation an der Basisklasse konfiguriert werden. Zur Verwendung der Concrete Table Inheritance ist keine weitere Konfiguration notwendig; um Single Table Inheritance oder Class Table Inheritance zu verwenden, können die Annotationen @ORM\InheritanceType("SINGLE_TABLE") bzw. @ORM\InheritanceType("JOINED") verwendet werden.

Je nach Anwendungsfall und Art der Daten eignen sich die einzelnen Strategien unterschiedlich gut. Die offizielle Doctrine-Dokumentation liefert hier eine gute Orientierungshilfe. Eine weitere sehr differenzierte Betrachtung der Vor- und Nachteile der einzelnen Muster stammt von Martin Fowler in dem Buch Patterns of Enterprise Application Integration.

Explizite Angabe von Spaltentypen

Standardmäßig ermittelt Doctrine den Spaltentyp aus der @var-Annotation eines Klassenattributs. Während dies in den meisten Fällen ganz gut funktioniert, kann es Situationen geben, in denen Ihr einen expliziten Spaltentyp angeben möchtet. Beispielsweise werden string-Attribute standardmäßig auf eine varchar(255)-Spalte abgebildet. Möchtet Ihr stattdessen lieber eine text-Spalte verwenden, kann dies über eine spezielle @ORM\Column-Annotation erfolgen:

Übrigens kann die @ORM\Column-Annotation noch weitaus mehr, als nur den Datentyp der Spalte zu verändern. Eine vollständige Beschreibung dieser Annotation findet sich im Doctrine-Handbuch.

Ausblick und weitere Informationsquellen

Leider kann ich in diesem Artikel nur einen Bruchteil der Funktionen beschrieben, die Doctrine bietet. Beispielsweise bietet Doctrine mit der Abfragesprache DQL eine sehr mächtige Möglichkeit, auf die in der Datenbank gespeicherten Objekte zuzugreifen (diese verdient jedoch noch einmal einen eigenen Artikel :-)). Als weitere Informationsquellen bieten sich beispielsweise das offizielle Doctrine-Handbuch oder der entsprechende Abschnitt aus dem FLOW3 Definitive Guide an. Für die theoretischen Hintergründe kann ich weiterhin das bereits zuvor erwähnte Buch Patterns of Enterprise Application Integration von Martin Fowler empfehlen.

Falls Ihr übrigens auf diesen Blog-Artikel gestoßen seid, weil ihr einfach nur nach „objektrelationalem Mapping“ gesucht und mit PHP und FLOW3 eigentlich gar nichts zu tun habt, gibt es beispielsweise mit Hibernate für Java oder SQLAlchemy für Python auch ORM-Frameworks für andere Programmiersprachen, die fast genauso arbeiten wie Doctrine.

Kommentare

  1. Gravatar
    Rene Wörz am

    Bei der m:n Bezieheung ist euch in der Klasse „Concert“ ein Fehler unterlaufen.
    Es sollte so aussehen:

    /**
    * @var \Doctrine\Common\Collections\Collection
    * @ORM\ManyToMany(inversedBy=“concerts“)
    */
    protected $artists;

    Antworten
  2. Gravatar
    Alexander Wende am

    Aber was wenn in der Abstrakten Klasse Beziehungen zu verschiedenen Klassen definiert werden sollen?
    Nehmen wir mal dein Beispiel:
    Abstrakte Klasse „Artist“
    – Konkrete Klasse „Comedian“
    – Konkrete Klasse „Musican“

    Angenommen die abstrakte Klasse Artist hätte noch die Eigenschaft „Vorbild“. Hier kann definiert werden welcher Künstler welchen anderen Künstler aus der gleichen Branche zum Vorbild hat.
    Wir bringt man Doctrine bei wann welche konkrete Klasse genommen werden soll?
    Wie würde sowas mit Flow aussehen?

    Antworten
  3. Gravatar
    Martin Keck am

    Was die Concrete Table Inheritence betrifft, empfehle ich die Verwendung der Annotation „@ORM\MappedSuperclass“ (s. auch den Link im Artikel zum Inheritance Mapping in Doctrine), andererseits schlagen SQL-Abfragen fehl. Möglich, dass das erst seit Flow 2.0 notwendig ist.
    Auch wird ohne diese Annotation eine überflüssige DB-Tabelle für die vererbende abstrakte Klasse erzeugt, sollte diese über Klassenparameter verfügen (was wiederum möglicherweise ursächlich für das Fehlschlagen der SQL-Abfragen ist).

    Antworten
  4. Gravatar
    Martin Brüggemann am

    Doctrine2 in Verbindung mit FLOW3 ist absolut wegweisend. Extrem nützlich ist auch die Option mit „@ORMHasLifecycleCallbacks“ Automatismen wie z.B. das automatische Solr-Index-Update beim Speichern eines Objekts durchführen zu lassen…

    Wofür braucht Mittwald eigentlich eine Konzert-Verwaltung? ;)

    Antworten

Kommentar hinzufügen