Generischer O/R-Mapper

1. Einleitung

In der objektorientierten Welt wird der Anspruch erhoben, Applikationen möglichst komplett objektorientiert entwerfen und entwickeln zu können. Um dieser Forderung gerecht zu werden, stößt jeder Entwickler unweigerlich auf das Problem, dass Daten in relationalen Datenbanken gehalten werden (müssen). Steht kein Hilfsmittel zur Verfügung, muss der DataMapper in jeder Applikation neu geschrieben werden. Das kostet nicht nur Zeit und Geld, sondern ist gegen den Ansatz "don't repeat yourself", denn diese Vorgehensweise produziert redundaten Quellcode.

Das APF-Modul genericormapper stellt eine Abstraktionsschicht zur Verfügung, die dem Entwickler einen Großteil der Mapping-Arbeit abnimmt. Der Mapper übernimmt dabei

  • Verwaltung von Objekten,
  • Verwaltung von Beziehungen zwischen Objekten (Komposition und Assoziation) und
  • CRUD-Funktionen auf Objekte und Objektstrukturen.

Für diese Aufgaben stehen eine Reihe von API-Funktionen zur Verfügung, die das Laden, Manipulieren und Löschen von definierten Objekten in der Datenbank abbilden. Das allgemeingültige Domänen-Objekt GenericDomainObject kann dabei entweder direkt verwendet oder nochmals innerhalb der Datenschicht der Applikation in die Domänen-Objekte der Anwendung übersetzt werden.

Die folgenden Kapitel zeigen, wie der O/R-Mapper konfiguriert und eingesetzt werden kann. Das im APF-Release enthaltene Modul usermanagement basiert auf dem O/R-Mapper und kann als weiterführendes Beispiel herangezogen werden. Das usermanagement-Modul wird unter Usermanagement-Modul näher beschrieben.

2. Konfiguration des O/R-Mappers

2.1. Grundlagen

Um den O/R-Mapper verwenden zu können, müssen zwei Konfigurationsdateien angelegt werden:

  • {ENVIRONMENT}_{NAMEAFFIX}_objects.ini
  • {ENVIRONMENT}_{NAMEAFFIX}_relations.ini

Dabei definiert die erste Datei die Objekte und deren Attribute, die zweite Konfigurationsdatei die Beziehungen zwischen den Objekten aus der ersten. Da der GenericORRelationMapper den ConnectionManager zum Aufbau der Datenbankverbindung nutzt, muss gegebenenfalls noch eine Sektion in der Datenbank-Verbindungskonfiguration angelegt werden.

Der Abschnitt {ENVIRONMENT} im Namen der beiden Konfigurationsdateien wird dabei dem Registry-Wert Environment aus dem Namespace APF\core entnommen, der Abschnitt {NAMEAFFIX} kann frei gewählt werden. Er dient als weiteres Unterscheidungsmerkmal und ermöglicht, dass unterschiedliche Mapper-Konfigurationen pro Applikation verwendet werden können. Letzeres ist vor allem dann interessant, wenn eine Applikation mehrere Datenquellen bedienen möchte/muss.

2.2. Konfigurationsbeispiel

Ein Entwickler möchte ein Gästebuch entwickeln. Die Quellcode-Dateien sind dabei im Namespace VENDOR\modules\myguestbook abgelegt und das Gästebuch benötigt nur einen O/R-Mapper. Weiterhin wurde der globale Registry-Wert Environment nicht manipuliert, die aktuelle Anwendung wird im Context sites\mysite ausgeführt und der Namenszusatz (NAMEAFFIX) lautet guestbook. In diesem Fall tragen die beiden Konfigurationsdateien den Namen

Code
DEFAULT_guestbook_objects.ini

sowie

Code
DEFAULT_guestbook_relations.ini

und müssen im Ordner

Code
/APF/config/modules/myguestbook/sites/mysite

abgelegt sein. Weitere Details zu Konfigurationsdateien, Namespaces und Kontext können im Kapitel Konfiguration nachgelesen werden.

2.3. Aufbau der Objekt- und Beziehungsdefinition

Die Syntax der Objekt- und Beziehungsdefinition gestaltet sich wie folgt:

2.3.1. Objektdefinition

Der GenericORRelationMapper stellt, wie bereits in der Einleitung angesprochen, ein allgemeingültiges Domänen-Objekt zur Verfügung (GenericDomainObject), das ein Objekt in der Datenhaltung repräsentiert. Der Typ des Objekts beschreibt sich dabei nicht durch den Klassennamen, sondern durch das Attribut ObjectName der Klasse.

Die Definition der Objekte beinhaltet daher lediglich den Namen des Objekts (=Name der Sektion) und die Attribute (=Properties der Klasse GenericDomainObject). Die folgende Codebox zeigt den Aufbau einer typischen Objektdefinition:

APF-Konfiguration
[Application] DisplayName = "VARCHAR(100)" [User] DisplayName = "VARCHAR(100)" FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" EMail = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" [Group] DisplayName = "VARCHAR(100)" [Role] DisplayName = "VARCHAR(100)"

Die Werte der Attribute bestimmen dabei die Auslegung der Felder in der Datenbank. Der Mapper kennt dabei die allgemeingültigen Werte

  • VARCHAR({LENGTH})
  • TEXT
  • DATE

die eigenständig in die entsprechenden SQL-Anweisungen "übersetzt" werden. Der Platzhalter {LENGTH} kann dabei durch eine beliebige Zeichenkettenlänge ersetzt werden. Alle darüber hinaus gehenden Feldtypen müssen ähnlich der Feldbeschreibung bei einer CREATE TABLE-Anweisung formuliert werden. Mit den hier aufgeführten Werten lassen sich jedoch die meisten Anwendungsfälle abbilden.

Der Generic O/R-Mapper unterstützt auch die Maskierung von BIT-Feldern. Hierzu muss der Wert einer Property-Definition eine gültige Definition eines BIT-Feldes wie z.B.
SQL-Statement
bit(7) NOT NULL default b'0'
Dabei ist es grundsätzlich nicht erheblich, dass das Feld einen Default-Wert besitzt. Wichtig ist die Definition eines BIT-Feldes mit dem Schlüsselwort "BIT". Details können der Foren-Diskussion unter Fehler mit BIT-Feldern entnommen werden.

Die Attribute eines beliebigen Objekts können dann wie folgt adressiert werden:

PHP-Code
... $user = new GenericDomainObject('User'); $user->setProperty('FirstName', 'Christian'); $user->setProperty('LastName', 'Achatz'); ... echo 'Vorname: ' . $user->getProperty('FirstName'); echo 'Name: ' . $user->getProperty('LastName'); ...
2.3.2. Beziehungsdefinition

Die Datei *_relations.ini definiert die Beziehungen zwischen den im vorherigen Kapitel beschriebenen Objekten. Der Mapper kennt dabei zwei Arten von Beziehungen: Komposition und Assoziation. Da Kompositionen im Gegensatz zu Assoziationen starke Bindungen sind, können Objekte, die weitere Objekte komponieren, nicht gelöscht werden, da sonst den komponierten Objekten die Existenzberechtigung entzogen werden würde. Dieser Fall wird vom Mapper deshalb mit einer entsprechenden Meldung quittiert.

Hinweis: Die Datenhaltungstheorie spricht bei der Auslegung der Beziehungen davon, dass jedes Objekt genau einmal komponiert sein soll, da es in der Realität nur eine starke Zugehörigkeit eines Objekts zu einem anderen geben kann. Weiterhin definiert eine Komposition eine Abhängigkeit oder auch Existenzberechtigung eines Objekts. Bei der Definition der Beziehungen muss daher darauf geachtet werden, dass abhängige Objekte entsprechend komponiert sind. Ein Gästebucheintrag kann beispielsweise nicht ohne ein Gästebuch existieren, der Benutzer, dem der Eintrag zugeordnet ist, dageben sehr wohl. In diesem Fall muss die Beziehung zwischen Gästebuch und Gästebucheintrag von der Qualität "Komposition" sein, die Beziehung zwischen Gästebucheintrag und dem Benutzer vom Typ "Assoziation".

Die folgende Codebox zeigt den Aufbau einer typischen Relationsdefinition:

APF-Konfiguration
[Application2Group] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Group" [Group2User] Type = "ASSOCIATION" SourceObject = "Group" TargetObject = "User" [Role2User] Type = "ASSOCIATION" SourceObject = "Role" TargetObject = "User" [Application2User] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "User" [Application2Role] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Role"

Der Sektionsname (z.B. Group2User) sollte sprechend gewählt werden, da dieser sowohl zum Laden von zu einem Objekt in Beziehung stehenden Objekten als auch für die Inbeziehungssetzung beim Speichern von Objekten Verwendung findet. Der Typ beinhaltet die Qualität der Beziehung, die Parameter SourceObject und TargetObject sind eine Referenz auf die Sektion der Objektdefinition.

Ebenfalls möglich sind Selbstreferenzen, also Referenzen zwischen ein und demselben Objekttyp. Hierbei besteht kein Unterschied zu normalen Referenzen, als Quell- und Ziel-Objekt wird einfach der selbe Objekt-Name eingetragen:

APF-Konfiguration
[User2BlockedUser] Type = "ASSOCIATION" SourceObject = "User" TargetObject = "User"

Auch bei der Verwendung muss nichts weiter beachtet werden, solange die SQL-Statements nicht manuell geschrieben werden, sondern über die Methoden des GenericORMapper erzeugt werden.

Hinweise:
  • Die Anzahl der Beziehungsdefinitionen ist nicht limitiert, die Definitionen sollten jedoch den Anforderungen der Applikation gerecht werden. Hierbei gilt die Daumenregel, dass bei mehrmaliger und gleichbedeutender Verwendung eines Attributs eines Objekts dieses in ein eigenes Objekt ausgelagert und das jeweilige Objekt in Beziehung (Assoziation) zu diesem gesetz werden soll. Typisches Beispiel ist die Sprache eines Objekts.
  • Wurde mit Hilfe der Methode addRelatedObject() ein Objektbaum aufgebaut, so können die in Beziehung stehenden Objekte mit der Methode getRelatedObjects() aus dem GenericDomainObject ausgelesen und ggf. weiterverarbeitet oder manipuliert werden.

2.4. Zusätzliche Indizes

Es ist möglich die Objekt-Definition mit zusätzlichen Indizes auszustatten. Dies kann aus Performance-Gründen notwendig sein und ist vor allem dann ratsam, wenn eine Property eines Objekts sehr häufig zur Abfrage von Daten genutzt wird.

In diesem Fall kann die die Objekt-Definition um den Schlüssel AddIndices erweitert werden. Diese Information wird vom automatischen Setup und Update dazu genutzt, weitere Indizes anzulegen um die Abfragen zu beschleunigen.

Die folgende Code-Box zeigt ein Beispiel für drei Indizes auf die wichtigsten Attribute eines Benutzer-Objekts:

APF-Konfiguration
[User] ... FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" ... AddIndices = "FirstName,LastName(INDEX)|Username(UNIQUE)|Password(INDEX)"

Die Definitionen unterliegen folgenden Regeln:

  • Jeder zusätzliche Index wird durch die zu inkludierenden Spalten und durch einen Typ ausgezeichnet. Mehrere Definitionen werden durch "|" (Pipe) getrennt.
  • Die zulässigen Index-Typen sind: INDEX (normaler Index), UNIQUE (Spalte darf nur eindeutige Werte enthalten) und FULLTEXT (Such-Index, der mit MATCH AGAINST() abgefragt werden kann). Die Index-Typen werden jeweils in Klammern notiert.
  • Sollen mehrere Spalten in den Index einbezogen werden, so können diese in einer Komma-separierten (",") Liste angegeben werden. Die Namen der Spalten entsprechen dabei den Namen in der Objekt-Definition.
Details zur Entstehung dieses Features können der Wiki-Seite Zusätzliche Indizes für Setup-/Update-Tool GORM und dem Foren-Thead Zusätzliche Indizes für Setup-/Update-Tool GORM entnommen werden.

2.5. Erzeugungsdatum von Beziehungen

Es ist möglich ein Erzeugungsdatum für Beziehungen im Datenmodell zu speichern. Zur Aktivierung des Features muss in der Beziehungskonfiguration das Attribut Timestamps auf den Wert TRUE gesetzt werden.

APF-Konfiguration
[{relation-name}] Type = "ASSOCIATION|COMPOSITION" SourceObject = "{source-table}" TargetObject = "{source-table}" Timestamps = "TRUE"
Bitte beachten Sie, dass das Feature für jede Beziehungsdefinition einzeln aktiviert werden muss.

Anschließend kann das Datenmodell mit Hilfe des GenericORMapperManagementTool erzeugt oder aktualisiert werden.

Bitte beachten Sie, dass für bestehende Beziehungen bei einem Update keine Werte nachgetragen werden. Beim Abfragen des Erzeugungsdatums erhalten Sie daher den Wert null.

Die Verwendung des Features ist in Kapitel 4.5 beschrieben.

2.6. Angabe der Speicher Engine pro Tabelle

Ab Version 2.1 ist es möglich bei Beziehungs- oder Object-Definitionen die zu verwendende Speicher-Engine anzugeben. Dazu muss in der Konfiguration im Attribut StorageEngine der Name der Speicher-Engine angegeben werden.

APF-Konfiguration
[{relation-name}] Type = "ASSOCIATION|COMPOSITION" SourceObject = "{source-table}" TargetObject = "{source-table}" StorageEngine = "MyISAM"
APF-Konfiguration
[User] ... FirstName = "VARCHAR(100)" LastName = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)" ... StorageEngine = "InnoDB"

Wird das Attribut StorageEngine nicht angegeben wird MyISAM oder die über setStorageEngine angegebene Engine benutzt.

3. Verwaltung der Datenbank

3.1. Erzeugung der Datenbank

Nachdem die Konfigurationsdateien fertig gestellt sind, muss die Datenbank für die Verwendung vorkonfiguriert werden. Hierzu bringt der GORM das GenericORMapperManagementTool mit. Mit Hilfe dieser Klasse lassen sich neue Datenbanken erzeugen und bestehende aktualisieren.

Das folgende Skript zeigt, wie das Datenbank-Setup mit Hilfe des GenericORMapperManagementTool das Layout der Tabellen automatisiert erstellt werden kann. Eine Vorlage für dieses Skript befindet sich zudem im Ordner /APF/modules/genericormapper/data/tools des jeweiligen apf-codepack-* Releases und trägt den Namen setup.php.

Um das Template zu verwenden, muss dieses gemäß den Bemerkungen unterhalb der Codebox für den entstprechenden Anwendungsfall angepasst werden. Das Setup-Skript im Überblick:

PHP-Code
// Einbinden der APF Bootstrap-Datei require('./APF/core/bootstrap.php'); // Optional: Definition Umgebung (wichtig für das Laden von Konfigurationen) use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperManagementTool; $setup = new GenericORMapperManagementTool(); // Context definieren (wichtig für das Laden von Konfigurationen) $setup->setContext('{CONTEXT}'); // Optional: anpassen der Storage Engine (Standard: MyISAM) //$setup->setStorageEngine('MyISAM|INNODB'); // Optional: anpassen des Daten-Typs für die Index-Spalte von Objekten //$setup->setIndexColumnDataType('INT(5) UNSIGNED'); // Initialisiert die Mapping-Konfiguration $setup->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // Initialisiert die Beziehungs-Konfiguration $setup->addRelationConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // Initialisiert die Datenbank-Verbindung (optional; falls nicht gesetzt werden // die generierten Statements direkt ausgegeben) $setup->setConnectionName('{CONNECTION_NAME}'); // Erzeugt das Datenbank-Layout direkt in der Datenbank $setup->run(true); // Gibt die Statements zur Erzeugung des Datenbank-Layouts aus $setup->run(false);

Die eingesetzten Platzhalter haben folgende Bedeutung:

  • {ENVIRONMENT}: Umgebungsvariable der Applikation. Diese wird bei der Adressierung von Konfigurationsdateien verwendet und muss auf den Wert gesetzt werden, der auch in der Zielanwendung verwendet wird. Details zur Definition von Konfigurationen mit dem APF können im Kapitel Konfiguration nachgelesen werden.
  • {CONTEXT}: Context der Applikation. Dieser wird zur Addressierung der Konfigurationsdateien verwendet und muss auf den Wert gesetzt werden, der auch in der Zielanwendung verwendet wird. Details zur Definition von Konfigurationen mit dem APF können im Kapitel Konfiguration nachgelesen werden.
  • {CONFIG_NAMESPACE}: Namespace, unter dem die Konfigurationsdateien für den O/R-Mapper liegen (siehe Kapitel 2.2).
  • {CONFIG_NAME_AFFIX}: Namenszusatz der Konfigurationsdateien (siehe Kapitel 2.1).
  • {CONNECTION_NAME}: Name der Datenbankverbindung, die für das Setup genutzt werden soll. Details zur Konfiguration von Datenbank-Verbindungen können Sie im Kapitel ConnectionManager nachlesen.
Bitte beachten Sie, dass die zu initialisierende Datenbank bereits existiert und der in der Verbindungskonfiguration aufgeführte Benutzer CREATE TABLE-Rechte für diese besitzt. Wird nach der Ausführung des Codes kein Fehler angezeigt, wurde das Setup erfolgreich abgeschlossen. Das Ergebnis kann dann beispielsweise mit phpMyAdmin oder dem phpMyAdmin überprüft werden.

Die Ausgabe des obigen Scripts sollte bei erfolgreicher Ausführung folgendes anzeigen:

SQL-Statement
CREATE TABLE IF NOT EXISTS `ent_application` ( `ApplicationID` INT(5) UNSIGNED NOT NULL auto_increment, `DisplayName` VARCHAR(100) character set utf8 NOT NULL default '', `CreationTimestamp` timestamp NOT NULL default CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (`ApplicationID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `ent_user` ( `UserID` INT(5) UNSIGNED NOT NULL auto_increment, `DisplayName` VARCHAR(100) character set utf8 NOT NULL default '', `FirstName` VARCHAR(100) character set utf8 NOT NULL default '', `LastName` VARCHAR(100) character set utf8 NOT NULL default '', `EMail` VARCHAR(100) character set utf8 NOT NULL default '', `Username` VARCHAR(100) character set utf8 NOT NULL default '', `Password` VARCHAR(100) character set utf8 NOT NULL default '', `CreationTimestamp` timestamp NOT NULL default CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (`UserID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ... CREATE TABLE IF NOT EXISTS `cmp_application2user` ( `CMPID` INT(5) UNSIGNED NOT NULL auto_increment, `Source_ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `Target_UserID` INT(5) UNSIGNED NOT NULL default '0', PRIMARY KEY (`CMPID`), KEY `JOININDEX` (`ApplicationID`,`UserID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `cmp_application2role` ( `CMPID` INT(5) UNSIGNED NOT NULL auto_increment, `Source_ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `Target_RoleID` INT(5) UNSIGNED NOT NULL default '0', PRIMARY KEY (`CMPID`), KEY `JOININDEX` (`ApplicationID`,`RoleID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Mit der Methode setIndexColumnDataType() kann der Daten-Typ der Spalten beeinflusst werden, die für die Speicherung der Objekt- und Beziehungs-Ids genutzt wird. In obigem Beispiel ist das INT(5) UNSIGNED. Sofern ein kleinerer oder größerer Daten-Raum benötigt wird, kann dies beispielsweise per
PHP-Code
$setup->setIndexColumnDataType('TINYINT(3)');
beeinflusst werden. Bitte beachten Sie, dass die in der Methode getätigten Angaben direkt in den Statements als Daten-Type genutzt werden. Sofern falsche Datentyp-Angaben genutzt werden, kommt es zu Fehlern bei der Erzeugung der Tabellen.

In phpMyAdmin sollte ungefähr folgende Ansicht erscheinen:

Ansicht der mit dem GORM erzeugten Tabellen im phpMyAdmin

Damit ist die Konfiguration des Mappers abgeschlossen und dieser kann in der Anwendung verwendet werden.

3.2. Aktualisierung der Datenbank

Das GenericORMapperManagementTool kann ebenfalls zur Übertragung der Änderungen an Konfigurations-Dateien auf eine vorhandene Datenbank genutzt werden.

Sofern kein automatisiertes Update gewüscht ist, bietet die Klasse GenericORMapperManagementTool zudem die Möglichkeit, die Update-Statements für eine manuelle Anwendung auszugeben. Für große Datenbanken wird ohnehin ein manuelles Update empfohlen, insbesondern wenn Indizes oder Spalten erzeut oder entfernt werden!

Das Update besitzt derzeit noch die Einschränkung, dass bei einigen MySQL-Versionen Spalten mit DEFAULT-Werten nochmals mit einem Update-Statement belegt werden, obwohl sich die Definition nicht ändert. Dies wird - sofern möglich - in den folgenden Versionen behoben. Die Tatsache stellt jedoch keine Einschränkung der Funktion des Updates dar!

Das folgende Skript zeigt, wie ein Datenbank-Update mit Hilfe des GenericORMapperManagementTool automatisiert vorgenommen werden kann. Eine Vorlage für dieses Skript befindet sich zudem im Ordner /APF/modules/genericormapper/data/tools des jeweiligen apf-codepack-* Releases und trägt den Namen update.php. Dieses muss gemäß den Bemerkungen unterhalb der Codebox für den entstprechenden Anwendungsfall angepasst werden. Hier das Setup-Skript im Überblick:

PHP-Code
// Einbinden der APF Bootstrap-Datei require('./APF/core/bootstrap.php'); // Umgebung konfigurieren (wichtig für das Laden von Konfigurationen) use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperManagementTool; $update = new GenericORMapperManagementTool(); // Context definieren (wichtig für das Laden von Konfigurationen) $update->setContext('{CONTEXT}'); // Optional: anpassen der Storage Engine (Standard: MyISAM) //$update->setStorageEngine('MyISAM|INNODB'); // Optional: anpassen des Daten-Typs für die Index-Spalte von Objekten //$update->setIndexColumnDataType('INT(5) UNSIGNED'); // Initialisiert die Mapping-Konfiguration $update->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // Initialisiert die Beziehungs-Konfiguration $update->addRelationConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // Initialisiert die Datenbank-Verbindung (optional; falls nicht gesetzt werden // die generierten Statements direkt ausgegeben) $update->setConnectionName('{CONNECTION_NAME}'); // Aktualisiert das Datenbank-Layout direkt in der Datenbank $update->run(true); // Gibt die Statements zur Aktualisierung des Datenbank-Layouts aus $update->run(false);

Die eingesetzten Platzhalter haben folgende Bedeutung:

  • {ENVIRONMENT}: Umgebungsvariable der Applikation. Diese wird bei der Adressierung von Konfigurationsdateien verwendet und muss auf den Wert gesetzt werden, der auch in der Zielanwendung verwendet wird. Details zur Definition von Konfigurationen mit dem APF können im Kapitel Konfiguration nachgelesen werden.
  • {CONTEXT}: Context der Applikation. Dieser wird zur Addressierung der Konfigurationsdateien verwendet und muss auf den Wert gesetzt werden, der auch in der Zielanwendung verwendet wird. Details zur Definition von Konfigurationen mit dem APF können im Kapitel Konfiguration nachgelesen werden.
  • {CONFIG_NAMESPACE}: Namespace, unter dem die Konfigurationsdateien für den O/R-Mapper liegen (siehe Kapitel 2.2).
  • {CONFIG_NAME_AFFIX}: Namenszusatz der Konfigurationsdateien (siehe Kapitel 2.1).
  • {CONNECTION_NAME}: Name der Datenbankverbindung, die für das Setup genutzt werden soll. Details zur Konfiguration von Datenbank-Verbindungen können Sie im Kapitel ConnectionManager nachlesen.

Weiterhin ist wichtig, dass die zu aktualisierende Datenbank bereits existiert und der in der Verbindungskonfiguration aufgeführte Benutzer CREATE TABLE-, ALTER TABLE- und, falls gewünscht, ALTER INDEX-Rechte für diese besitzt. Wird nach der Ausführung des Codes kein Fehler angezeigt, wurde das Setup erfolgreich abgeschlossen. Das Ergebnis kann dann beispielsweise mit phpMyAdmin überprüft werden.

Bitte beachten Sie folgende wichtige Grundregeln:
  • Große Datenbanken sollten keinesfalls automatisiert aktualisiert werden!
  • Bei der Umbenennung von Spalten werden spezifisch angelegte Indizes nicht beachtet und u.U. gelöscht!

4. Verwendung des O/R-Mappers

Der O/R-Mapper, oder genauer die Komponente GenericORRelationMapper, bietet eine Reihe von API-Methoden an, die zur Manipulation von Daten und Beziehungen eingesetzt werden können. Hier ein Überblick über die Methoden, deren Parameter und Bedeutung:

  • loadObjectListByCriterion(): Läd eine Liste von Objekten an Hand eines Kriterien-Objekts.
  • loadObjectByCriterion(): Läd ein Objekt an Hand eines Kriterien-Objekts.
  • loadRelatedObjects(): Läd eine Liste von Objekten, die mit diesem über eine definierte Beziehung verknüpft sind.
  • loadNotRelatedObjects(): Läd eine Liste von Objekten, die mit diesem über nicht über eine definierte Beziehung verknüpft sind.
  • loadRelationMultiplicity(): Läd die Anzahl der zu einem Objekt verküpften Objekte unter Angabe der Beziehungskennung.
  • saveObject(): Speichert ein Objekt oder einen Objektbaum, der aus in Beziehung stehenden Domain-Objekten besteht.
  • deleteObject(): Löscht ein Objekt. Dabei werden bestehende Assoziationen und Kompositionen aufgelöst.
  • createAssociation(): Erzeugt eine Assoziation zwischen zwei Objekten.
  • deleteAssociation(): Löscht die Assoziation zwischen zwei Objekten.
  • deleteAssociations(): Löscht alle Assoziation, die ein Objekt ausbildet.
  • isAssociated(): Prüft, ob eine Assoziation zwischen zwei Objekten besteht.
  • loadObjectList(): Lädt eine Liste von sämtlichen Objekten.
  • loadObjectListByStatement(): Lädt eine Liste von Objekten an Hand eines Statements.
  • loadObjectListByTextStatement(): Lädt eine Liste von Objekten an Hand eines übergebenen SQL-Statements.
  • loadObjectListByIDs(): Lädt eine Liste von Objekten an Hand eines übergebenen Arrays.
  • loadObjectByStatement(): Lädt ein Objekt an Hand eines Statements.
  • loadObjectByTextStatement(): Lädt eine Liste von Objekten an Hand eines übergebenen SQL-Statements.
  • loadObjectByID(): Lädt ein Objekt an Hand einer übergebenen ID.
  • loadObjectsWithRelation(): Lädt eine Liste von Objekten, die durch den Objekt-Typ und die ausgeprägte Beziehung limitiert ist.
  • loadObjectsWithoutRelation(): Lädt eine Liste von Objekten, die durch den Objekt-Typ und die - in diesem Fall - nicht ausgeprägte Beziehung limitiert ist.
  • loadRelatedObject(): Lädt ein Objekt, das zum aktuellen Objekt über die bei Aufruf definierte Beziehung verbunden ist.

Die *Statement*-Methoden werden aus Performance-Gründen angeboten. Datails zu Argumenten und Rückgabewerten können der API-Dokumentation entnommen werden. Im Folgenden finden die wichtigsten Methoden jedoch Verwendung.

4.1. Erzeugen einer Instanz

4.1.1. Klassische Vorgehensweise
Bitte beachten Sie, dass die hier beschriebene Vorgehensweise mit der Version 1.17 als veraltet markiert wurde. Für eine optimale Nutzung der Möglichkeiten des O/R-Mappers empfehlen wir Ihnen bereits mit Release 1.17 den Umstieg auf die Erzeugung mit dem DIServiceManager.

Die Instanz eines O/R-Mappers muss über die zugehörige Factory (GenericORMapperFactory) erzeugt werden. Dies ist zum einen deshalb notwendig, um den konkreten O/R-Mapper vor der Verwendung zu initialisieren und zum anderen, damit mehrere O/R-Mapper innerhalb einer Applikation verwendet werden können. Letzteres ist in einfachen Anwendungen sicher nicht notwenig, in komplexeren Konstrukten ist dies jedoch eine notwendige Anforderung.

Die folgende Codebox zeigt einen typischen Aufruf eines O/R-Mappers:

PHP-Code
// Factory im relevanten Service-Mode erstellen $ormFact = &$this->getServiceObject( 'APF\modules\genericormapper\data\GenericORMapperFactory'[, {SERVICE_OBJECT_TYPE}] ); // Mapper von der Factory beziehen $orm = &$ormFact->getGenericORMapper( {CONFIG_NAMESPACE}, {CONFIG_NAME_AFFIX}, {CONNECTION_NAME}[, $logStatements = false] );

Die Platzhalter haben dabei folgende Bedeutung:

  • {SERVICE_OBJECT_TYPE}: Art der Instanziierung der Factory. Dies bestimmt ebenfalls den Service-Typ des erzeugten Mappers. Gültige Werte sind NORMAL, SINGLETON und SESSIONSINGLETON, Standard ist SINGLETON. Details können dem Kapitel Services entnommen werden.
  • {CONFIG_NAMESPACE}: Namespace, unter dem die Konfigurationsdateien für den O/R-Mapper liegen (siehe Kapitel 2.2).
  • {CONFIG_NAME_AFFIX}: Namenszusatz der Konfigurationsdateien (siehe Kapitel 2.1).
  • {CONNECTION_NAME}: Name der Datenbankverbindung, die für das Setup und die produktive Verwendung genutzt werden soll.

Bitte beachten Sie, dass die Factory den Service-Typ des Mappers (=Gültigkeitsbereich des Objekts) definiert. Soll der GORM aus Performance-Gründen innerhalb einer Benutzer-Sitzung nur einmal erstellt werden - dies ist sinnvoll, da Mapping- und Beziehungs-Tabellen nur einmal initialisiert werden -, so muss bei der Erzeugung der Factory der dritte Paramater (Service-Typ) mit dem Wert SESSIONSINGLETON befüllt werden.

Für Entwicklungs-Umgebungen empfiehlt es sich den Service-Typ auf den Wert SINGLETON einzustellen. Andernfalls werden Änderungen der Mapping- oder Beziehungs-Definitionen erst nach Ablauf der Session aktiv.

Weitere Hinweise finden sich auf der Wiki-Seite Typische Fehler beim GenericORMapper.

Wichtig ist dabei weiterhin, dass die Factory mit der Methode getServiceObject() erzeugt wird, da es sonst zu unerwünschten Seiteneffekten hinsichtlich Konfiguration der Mapper kommen kann.

Um Statement-Logging für Debug-Zwecke zu aktivieren, muss der optionale Parameter $logStatements mit dem Wert true belegt werden. Diese Option sollte im Live-Betrieb jedoch nicht verwendet werden! Details zum Parameter können der API-Dokumentation entnommen werden.
4.1.2. Erzeugung via DI

Um Konfigurationen und Abhängigkeiten zu kapseln wird empfohlen, den O/R-Mapper mit dem DIServiceManager zu erzeugt werden. Diese Vorgehensweise hat ebenfalls deutliche Vorteile für die Testbarkeit einer Komponente und die Entkopplung der Business- von der Präsentations-Schicht.

Die Erzeugung des GORM-Service erfolgt dabei direkt über den DIServiceManager und nicht über die oben beschriebene Factory. Grund hierfür ist, dass der DIServiceManager nur explizite Services zur dynamischen Inititialisierung eines anderen Services akzeptiert. Dazu existieren drei Service-Implementierungen, die zwar keine Funktion tragen, jedoch die notwendige Konfigurations-Information in den GORM tragen:

  • GenericORMapperDIConfiguration: Injektion der Basis-Konfiguration
  • GenericORMapperDIMappingConfiguration: Injektion von zusätzlichen Objekt-Konfigurationen
  • GenericORMapperDIRelationConfiguration: Injektion von zusätzlichen Beziehungs-Konfigurationen
Details zur Konfiguration und ein Anwendungsbeispiel finden sich im Wiki unter Erzeugen des GORM mit dem DIServiceManager.

Für die Beispiele in den folgenden Kapitel gehen wir davon aus, dass eine Konfiguration für den Service OR-Mapper unter dem Service-Namespace VENDOR\data\mapper vorhanden ist:

APF-Konfiguration
[OR-Mapper] servicetype = "SINGLETON" class = "APF\modules\genericormapper\data\GenericORRelationMapper" setupmethod = "setup" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.debug.method = "setDebugMode" conf.debug.value = "true|false" init.db.method = "setDbDriver" init.db.namespace = "VENDOR\data\mapper" init.db.name = "DATABASE" init.rel.method = "addDIRelationConfiguration" init.rel.namespace = "VENDOR\data\mapper" init.rel.name = "CONFIG-RELATION" init.map.method = "addDIMappingConfiguration" init.map.namespace = "VENDOR\data\mapper" init.map.name = "CONFIG-MAPPING" [CONFIG-MAPPING] servicetype = "NORMAL" class = "APF\modules\genericormapper\data\GenericORMapperDIMappingConfiguration" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.affix.method = "setConfigAffix" conf.affix.value = "..." [CONFIG-RELATION] servicetype = "NORMAL" class = "APF\modules\genericormapper\data\GenericORMapperDIRelationConfiguration" conf.namespace.method = "setConfigNamespace" conf.namespace.value = "VENDOR\data\mapper" conf.affix.method = "setConfigAffix" conf.affix.value = "..." [DATABASE] servicetype = "SINGLETON" class = "APF\core\database\MySQLiHandler" setupmethod = "setup" conf.host.method = "setHost" conf.host.value = "..." conf.name.method = "setDatabaseName" conf.name.value = "..." conf.user.method = "setUser" conf.user.value = "..." conf.pass.method = "setPass" conf.pass.value = "..."

Diese kann nun als Grundlage genutzt werden um die Instanz des O/R-Mappers mit dem Aufruf

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper');

zu erzeugen.

4.2. Laden von Daten

Um die Beschreibung der Features plastischer gestalten zu können, soll folgendes UML als Basis für Beispiele dienen. Das Diagramm enthält die Definition der Business-Objekte des Usermanagement-Modul-Moduls. Die im Kapitel 4.2. verwendeten Code-Beispiele sind dabei dem genannten Modul entnommen.

Bitte beachten Sie, dass der O/R-Mapper nur diejenigen Objekte verwalten kann, die der aktuellen Instanz über die Konfiguration bekannt sind. Die Liste der Objekte und Beziehungen kann wie in Kapitel 6 beschrieben durch Hinzufügen von weiteren Konfigurationen erweitert werden.

Weitere Hinweise hierzu finden sich im Forum unter The object name "Application" does not exist ....

APF user management domain model

4.2.1. Laden von Objekten

Für das Laden von Objekten stehen die Methoden

  • loadObjectByCriterion()
  • loadObjectByTextStatement()
  • loadObjectByStatement()
  • loadObjectByID()

zur Verfügung. Möchte der Entwickler auf einer Seite die Details eines Benutzers (siehe UML-Diagramm) darstellen, so können die aufgeführten Methoden wie in der anschließend dargestellten Codebox beschreiben eingesetzt werden:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer laden (1) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('UserID',1); $User = $ORM->loadObjectByCriterion('User',$Crit); // Benutzer laden (2) $select = 'SELECT * FROM ent_user WHERE UserID = \'1\';'; $User = $ORM->loadObjectByTextStatement('User',$select); // Benutzer laden (3) $User = $ORM->loadObjectByStatement('User', 'APF\modules\usermanagement', 'load_user_by_id'); // Benutzer laden (4) $User = $ORM->loadObjectByID('User',1);

Der Inhalt der Statement-Datei load_user_by_id ist dabei

SQL-Statement
SELECT * FROM ent_user WHERE UserID = '1';
Details zur Ausführung von Statement-Dateien können dem Kapitel Statement-Dateien entnommen werden.

die Klasse GenericDomainObject verfügt über einige Methoden zur Vereinfachung der Implementierung. Über
PHP-Code
$user = $orm->loadObjectByID('User', 1); echo $user->getObjectId();
lässt sich sehr einfach die ID des Objektes ohne Umweg über die Properies auslesen. Mit dem Pendant setObjectId() kann die Id des Objektes gleichermaßen gefüllt werden. Per
PHP-Code
$user = $orm->loadObjectByID('User', 1); echo $user->getObjectName();
kann der Name des Objekts angezeigt werden. Wird ein mit dem GORM geladenes Objekt per
PHP-Code
$user = $orm->loadObjectByID('User', 1); echo $user;
ausgegeben, wird die String-Repräsentation des Objektes dargestellt.
4.2.2. Laden von Objekt-Listen

Für das Laden von Objekt-Listen stehen die Methoden

  • loadObjectList()
  • loadObjectListByCriterion()
  • loadObjectListByTextStatement()
  • loadObjectListByStatement()
  • loadObjectListByIDs()

zur Verfügung. Möchte der Entwickler auf einer Seite eine Liste von Benutzern (siehe UML-Diagramm) darstellen, so können die aufgeführten Methoden wie in der anschließend dargestellten Codebox beschreiben eingesetzt werden:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer-List laden (1) $UserList = $ORM->loadObjectList('User'); // Benutzer-Liste laden (2) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('DisplayName','a%'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // Benutzer-Liste laden (3) $select = 'SELECT * FROM ent_user WHERE DisplayName LIKE \'a%\';'; $UserList = $ORM->loadObjectListByTextStatement('User',$select); // Benutzer-Liste laden (4) $UserList = $ORM->loadObjectListByStatement('User', 'APF\modules\usermanagement', 'load_user_list'); // Benutzer-Liste laden (5) $UserList = $ORM->loadObjectListByIDs('User',array(1,2,3,4,5,6));

Der Inhalt der Statement-Datei load_user_list ist dabei

SQL-Statement
SELECT * FROM ent_user WHERE DisplayName LIKE 'a%';
4.2.3. Nachladen von Beziehungsobjektlisten

Besteht die Notwendigkeit, bei der Auflistung der Benutzer, deren zugeordnete Gruppen mit aufzuführen, können die Gruppen an Hand der Beziehung nachgeladen werden. Für das Nachladen von zu einem Objekt in Beziehung stehenden Objekten kann die Methode

  • loadRelatedObjects()

eingesetzt werden. Das folgende Beispiel zeigt, wie die einem Benutzer zugeordneten Gruppen geladen werden können:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer-Liste laden $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // Ausgeben der Liste inkl. Gruppen des Benutzers for($i = 0; $i < count($UserList); $i++){ // Name des Benutzers ausgeben echo $UserList[$i]->getProperty('DisplayName'); // Gruppen nachladen $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User'); // Gruppen ausgeben echo ' ,Gruppen: '; for($j = 0; $j < count($GroupList); $j++){ echo $GroupList[$j]->getProperty('DisplayName').' '; } }

Zur Vereinfachung des Nachladens besitzt auch das Objekt GenericDomainObject die Methode loadRelatedObjects(). Damit ist es möglich in der Präsentationsschicht, und überall dort, wo keine Instanz des Mappers zur Verfügung steht, in Beziehung stehende Objekte nachzuladen. Im obigen Beispiel können die einem Benutzer zugeordneten Gruppen damit auch per

PHP-Code
$GroupList = $UserList[$i]->loadRelatedObjects('Group2User');

geladen werden.

Hinweis: Die Menge der nachgeladenen Daten kann auch hier mit einem GenericCriterionObject eingeschränkt werden. Die im Beispiel genannte Gruppen-Liste kann wie folgt limitiert werden:

PHP-Code
// Definieren der Limitierungsindikatoren $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $Crit->addPropertyIndicator('DisplayName','A%'); $Crit->addCountIndicator(10); // Laden der Liste ueber das Domänen-Objekt selbst $GroupList = $UserList[$i]->loadRelatedObjects('Group2User',$Crit); // Laden der Liste direkt ueber den O/R-Mapper $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User',$Crit);
4.2.4. Nachladen von "Nichtbeziehungsobjekten"

Oft besteht die Notwendigkeit, Objekte zu selektieren, die zu einem bestimmten Objekt (noch) nicht in Beziehung stehen, für die jedoch eine Beziehung definiert ist. Ein konkreter Anwendungsfall bezogen auf das oben gezeigte UML-Diagramm ist die Selektion aller Gruppen, zu denen ein Benutzer noch keine Assoziation hat um diesen zur Gruppe hinzufügen zu können. Zu diesem Zweck kann die Methode

  • loadNotRelatedObjects()

eingesetzt werden. Das folgende Beispiel zeigt, wie alle Gruppen selektiert werden können, zu denen der genannte Benutzer noch keine Beziehung besitzt:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer selektieren $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // Selektieren der nicht assoziierten Gruppen $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User'); // Ausgeben der Liste der noch nicht assoziierten Gruppen for($i = 0; $i < count($GroupList); $i++){ echo '' . $GroupList[$i]->getProperty('DisplayName'); }

Hinweis: Auch hier kann die Menge der nachgeladenen Objekte mit Hilfe des GenericCriterionObject eingeschränkt werden. Häufiger Anwendungsfall ist hier die Einschränkung über weitere Beziehungen der gewünschten Objekte zu anderen. Im folgenden Beispiel sollen nur diejenigen Gruppen selektiert werden, zu denen der gewühlte Benutzer noch keine Beziehung besitzt, die jedoch unterhalb eines definierten Application-Objekts komponiert sind:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer selektieren $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // Additived Beziehungskriterium definieren $Crit = new GenericCriterionObject(); $App = new GenericDomainObject('Application'); $App->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2Group',$App); // Selektieren der nicht assoziierten Gruppen $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User',$Crit); // Ausgeben der Liste der noch nicht assoziierten Gruppen for($i = 0; $i < count($GroupList); $i++){ echo '' . $GroupList[$i]->getProperty('DisplayName'); }

4.2.5 Laden der Beziehungsmultiplizität

Um herauszufinden, wie viele Objekte in Beziehung zu einem anderen gesetzt wurden, steht dem Entwickler die Methode

  • loadRelationMultiplicity()

zur Verfügung. Diese gibt die erfragte Anzahl an Hand eines Objekts und eines Beziehungsschlüssels zurück. Soll die Anzahl der Benutzer einer Gruppe abgefragt werden, so kann dies mit folgendem Code bewerkstelligt werden:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Gruppe selektieren $Group = $ORM->loadObjectByID('Group',1); // Selektieren und Ausgeben der Anzahl der Benutzer einer Gruppe echo $ORM->loadRelationMultiplicity($Group,'Group2User');

4.2.6 Laden der Objekt-Anzahl

Neben der Anzahl der zu einem Objekt in Beziehung stehenden Objekte können seit dem Release 1.12 auch die Anzahl der in der Datenbank befindlichen Objekte eines definierten Typs abgefragt werden. Hierzu steht die Methode

  • loadObjectCount()

zur Verfügung. Als Parameter wird der Name des Objekt gemäß der Definition der Objekte in der Konfiguration erwartet. Optional kann noch ein GenericCriterionObject mitgegeben werden, das das Ergebnis auf Basis von Attributen des Objekts einschränken kann.

Die Abfragen aller Objekte vom Typ User und aller Benutzer mit dem Buchstaben A am Anfang des Nachnamens können wie folgt durchgeführt werden:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); $totalUsers = $orm->loadObjectCount('User'); $crit = new GenericCriterionObject(); $crit->addPropertyIndicator('LastName','A%'); $usersWithA = $orm->loadObjectCount('User',$crit);
Da die Abfrage ungecached gegen die Datenbank abgesendet wird, sollte die Abfrage der Objekt-Anzahl nicht in Performance-kritischen Bereichen der Applikation durchgeführt werden.

4.3. Speichern von Objekten

Für das Speichern von Objekten steht die Methode

  • saveObject()

zur Verfügung. Um einen Benutzer in der Datenbank zu speichern ist folgender Code notwendig:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Benutzer befuellen $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // Benutzer speichern $orm->saveObject($user);
Das Objekt (in diesem Fall $user) kann nach dem Speichern direkt weiter verwendet werden. Der Mapper injiziert diesem bereits die aktuelle Mapper-Instanz und die ID des Objektes in der Datenbank. Details zum Feature-Request können dem Foren-Eintrag Erweiterung GORM (Release 1.11) entnommen werden.

4.4. Speichern von Objekt-Bäumen

Wie bereits in der Einleitung angemerkt, kann der O/R-Mapper nicht nur einzelne Objekte, sondern auch Objektbäume speichern. Dieses Feature kann in der Datenschicht der Applikation insbesondere dazu genutzt werden, um für die Applikation notwendige Beziehungen aufzubauen.

Aufgabenstellung: Beim Erstellen eines Benutzers, soll dieser unterhalb einer Applikation komponiert werden. Diese Komposition kann später dazu genutzt werden um das Usermanagement mandantenfähig zu gestalten.

Umsetzung: Um eine Beziehung zwischen einem Application- und einem User-Objekt herzustellen und diese Beziehung auch zu speichern, kann die Methode addRelatedObject() der Klasse GenericDomainObject verwendet werden. Die folgende Codebox zeigt die Implementierung:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Applikation laden $app = $ORM->loadObjectByID('Application',1); // Benutzer befuellen $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // Beziehung herstellen $app->addRelatedObject('Application2User',$user); // Objektbaum speichern $orm->saveObject($app);

Das Anlegen von Beziehungen des Typs Komposition muss exakt die im Quellcode vorgestellte Art der Beziehungs-Generierung verwendet werden. Dies ist der Fall, da ein komponiertes Objekt nicht ohne sein Vater-Objekt leben kann. Assoziationen können auch nachträglich per createAssociation() angelegt werden.

Sofern Objekte neu angelegt werden sollen - wie oben der Benutzer - ist es nicht notwendig, diesen vorher selbst zu speichern. Dies übernimmt der GORM implizit beim Speichern des kompletten Baumes (bestehend aus der Application, dem User und der Beziehung, die den Benutzer unter der Applikation komponiert).

Möchte der Entwickler im gleichen Zug dem Benutzer noch eine Gruppe und eine Rolle zuordnen, muss der oben gezeigte Quellcode zwischen dem Befüllen des Benutzer-Objekts und der Herstellung der Beziehung zum Application-Objekt entsprechend erweitert werden:

PHP-Code
// Applikation laden $app = $orm->loadObjectByID('Application',1); // Benutzer befuellen $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // Gruppe laden $group = $orm->loadObjectByID('Group',1); // Rolle laden $role = $orm->loadObjectByID('Role',1); // Gruppe und Rolle zuweisen $user->addRelatedObject('Group2User',$group); $user->addRelatedObject('Role2User',$role); // Beziehung herstellen $app->addRelatedObject('Application2User',$user); // Objektbaum speichern $orm->saveObject($app);

Der GORM ist in der Lage, beliebig große Objekt-Bäume zu speichern. Hierbei gilt es jedoch zu beachten, dass sehr große Bäme aus Performance-Gründen etwas anders behandelt werden sollten. Dies ist jedoch nur Anwendungen notwendig, die sehr hohen Performance-Anforderungen unterliegen. In der Regel ist die Performance des GORM in der oben beschriebenen Vorgehensweise absolut ausreichend.

Sofern die Anzahl der bei einer Speicherung involvierten Objekte sehr groß wird (c.a. 20 Objekte mit jeweils mind. 1 Assoziations-Beziehung) ist zu empfehlen, die Objekte ohne das Aufbauen der Beziehungen mit einem Aufruf von saveObject() zu speichern und diese anschließend per createAssociation() anzulegen. Diese Art der Optimierung kann für Kompositionen nicht genutzt werden. Dies ist in der Bedeutung der Komposition begründet.

4.5. Erzeugungsdatum von Beziehungen

Ab Release 1.16 ist es möglich, das Erzeugungsdatum einer Beziehung zwischen zwei Objekten abzufragen. Hierzu muss das Feature zunächst wie in Kapitel 2.5 beschrieben pro Beziehung aktiviert werden.

Anschließen können Sie das Erstellungsdatum einer Beziehung wie folgt abgefragen:

PHP-Code
$car = $gorm->loadObjectByID('Car', 1); $wheels = $car->getRelatedObjects('Car2Wheels'); foreach ($wheels AS $wheel) { echo 'Wheel mounted at: ' . $wheel->getRelationCreationTimestamp(); }

Als Rückgabewert der Methode getRelationCreationTimestamp() erhalten Sie ein Datum im MySQL-Timestamp-Format. Dieses kann anschließend mit Hilfe der PHP-Date-API formatiert werden.

Sofern kein Erstellungsdatum vorhanden ist, erhalten Sie den Wert null. Dies ist üblicherweise dann der Fall, wenn das Feature für die aktuell zur Abfrage genutzte Beziehung nicht aktiviert ist oder das Feature durch ein späteres Update bei bestehenden Daten aktiviert wurde.

5. Übersicht zum GenericCriterionObject

Das vorliegende Kapitel möchte einen zusammenfassenden Überblick über die Nutzung des GenericCriterionObject geben. Wie in den vorherigen Kapiteln angedeutet, kann das Kriterium-Objekt dazu genutzt werden, Abfragen ohne Schreiben von SQL-Statements für den Anwendungsfall zu konfigurieren. Das Objekt kann bei den load*ByCriterion()-Methoden und beim Nachladen von in Beziehung stehenden Objekten und Objektlisten genutzt werden.

5.1. Grundlagen

Die folgende Code-Box zeigt einen Überblick über die Einsatzmöglichkeiten des GenericCriterionObjects am Beispiel einer Benutzer-Liste, deren Benutzer zu einer Applikation gehören und eine definierte Gruppe zugeordnet haben:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; class UsermanagementManager extends APFObject { public function getUserList() { /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Erzeugen des Kriterien-Objekts $Crit = new GenericCriterionObject(); // Hinzufuegen einer Beziehung zum Objekt "Application" (Komposition) $Application = new GenericDomainObject('Application'); $Application->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2User',$Application); // Hinzufuegen einer Beziehung zum Objekt "Group" (Assoziation) $Group = new GenericDomainObject('Group'); $Group->setProperty('GroupID',1); $Crit->addRelationIndicator('Group2User',$Group); // Hinzufuegen einer Begrenzung der Anzahl mit definiertem Startpunkt $Crit->addCountIndicator(0,3); // Hinzufuegen einer Bedingung auf Ebene der Eigenschaften des zu ladenden Objekts $Crit->addPropertyIndicator('LastName','Achatz'); // Hinzufuegen einer Sortierreihenfolge $Crit->addOrderIndicator('FirstName','ASC'); $Crit->addOrderIndicator('LastName','DESC'); // Definition der zu ladenden Attribute eines Objekts $Crit->addLoadedProperty('FirstName'); $Crit->addLoadedProperty('LastName'); // Laden einer Objektliste mit Hilfe des Kriterium-Objekts return $ORM->loadObjectListByCriterion('User',$Crit); // Laden eines Objekts mit Hilfe des Kriterium-Objekts return $ORM->loadObjectByCriterion('User',$Crit); } }

Hinweise zum Quelltext:

  • Beziehungen: Das Hinzufügen von Beziehungen zum Kriterien-Objekt beschreiben, dass das zu ladende Objekt oder jedes Objekt der zu ladenden Liste in Beziehung zum Objekt des Kriteriums stehen muss. Wird wie im Beispiel eine Beziehung zum Objekt Application (Komposition) und zum Objekt Group (Assoziation) aufgebaut, ist das Ergebnis eine Liste von Objekten innerhalb einer Applikation, die in einer bestimmten Gruppe sind.
    Möchte der Entwickler alle Benutzer selektieren, die in einer Applikation enthalten sind, einer definierten Gruppe angehören und eine bestimmte Rolle zugewiesen haben, müssen drei Beziehungen gemäß der Beziehungskonfiguration zum Kriterium hinzugefügt werden.
  • Sortierreihenfolge: Die Reihenfolge der Aufrufe entscheidet die Sortierung. Soll die Sortierung in einer anderen Reihenfolge vorgenommen werden, müssen die Sortierkriterien in der entsprechend anderen Abfolge hinzugefügt werden. Der Wert ASC steht für aufsteigende Sortierung, DESC für absteigende.
Die Klasse GenericCriterionObject bietet ein fluent interface an. Dieses erlaubt es verschiedene Aufrufe direkt aneinander zu reihen:
PHP-Code
$criterion = new GenericCriterionObject(); $criterion ->addCountIndicator(1) ->addRelationIndicator('xxx', $sourceObject) ->addOrderIndicator('name') ->addPropertyIndicator($attributeName, $attributeValue);
Weitere Hinweise finden sich unter Fluent Interface des GenericCriterionObject.

5.2. Verschachtelung

Die Klasse GenericCriterionObject bietet die Möglichkeit Bedingungen nicht nur mit AND, sondern auch mit den anderen logischen Operatoren OR, XOR und NOT zu verknüpfen. Hierbei war es notwendig eine Verschachtelung zu integrieren, die einem die Möglichkeit gibt Abfragen mit einer Mischung verschiedener Operatoren zu erstellen.

Zu diesem Zweck wurde die Methode setLogicalOperator() hinzugefügt. Standardmäßig ist die Verknüpfungsart auf AND gesetzt, um eine abwärtskompatibilität zu gewährleisten. Das Setzen des logischen Operators erfolgt als String:

PHP-Code
$criterion = new GenericCriterionObject(); $criterion->setLogicalOperator('OR');
Das fluent interface findet natürlich auch in dieser Methode Anwendung.

Der logische Operator setzt allerdings nicht den globalen Operator für die komplette Instanz sondern wird lediglich zwischengespeichert und erst beim Hinzufügen weiterer PropertyIndicator angewendet.

PHP-Code
$criterion = new GenericCriterionObject(); $criterion->addPropertyIndicator('feld1','wert1') ->addPropertyIndicator('feld2','wert2') ->setLogicalOperator('OR') ->addPropertyIndicator('feld3','wert3') ->addPropertyIndicator('feld4','wert4');

Dieser Code erzeugt dabei folgende Bedingung (Die Tabellenbezeichnung, die der GORM automatisch hinzufügt lasse ich aus Gründen der Übersicht weg):

Code
[...] WHERE `feld1`='wert1' AND `feld2`='wert2' OR `feld3`='wert3' OR `feld4`='wert4'

Der Operator bleibt also bis zur nächsten Änderung gespeichert.

Es ist aber nicht in allen Fällen sinnvoll die Verknüpfungen direkt hintereinander zu setzen, denn eine Bedingung wie die obige würde auch zum Erfolg führen, wenn nur `feld4` dem Wert 'wert4' entspricht. Würde man die Bedingung aber gerne so aufbauen, dass `feld1` immer dem Wert 'wert1' entsprechen soll und von den Bedingungen der drei anderen Felder mindestens eine wahr sein soll, müssten die Bedingungen der letzten drei Felder eingeklammert werden. Dieses Ziel wird mit folgendem Code erreicht:

PHP-Code
$criterion1 = new GenericCriterionObject(); $criterion2 = new GenericCriterionObject(); $criterion2->setLogicalOperator('OR') ->addPropertyIndicator('feld2','wert2') ->addPropertyIndicator('feld3','wert3') ->addPropertyIndicator('feld4','wert4'); $criterion1->addPropertyIndicator('feld1','wert1') ->addPropertyIndicator('feld2+feld3+feld4',$criterion2);

Hier ist zu sehen, dass als zweiter Parameter der Methode addPropertyIndicator() ein Objekt der Klasse GenericCriterionObject übergeben wird. Diese Verschachtelung ist hierarchisch nicht begrenzt und kann somit beliebig tief angewendet werden. Der oben gezeigte Code erzeugt also den gewünschten SQL-Code:

Code
[...] WHERE `feld1`='wert1' AND (`feld2`='wert2' OR `feld3`='wert3' OR `feld4`='wert4')
Die Methode addPropertyIndicator() erwartet als zweiten Parameter normalerweise Text oder Zahlenwerte weshalb dieser Parameter nicht als Referenz entgegengenommen wird. Grund hierfür ist, dass ansonsten keine Werte direkt übergeben werden könnten sondern stattdessen jeder Wert erst in einer Variablen gespeichert werden muss, um dann die Variable zu übergeben. Das wäre überaus unpraktisch, weshalb an dieser Stelle bewusst auf eine Referenzierung verzichtet wird. Entsprechend muss man nun allerdings beachten, dass die übergebene Instanz bei einer Verschachtelung ebenfalls nicht als Referenz übergeben wird. Das bedeutet, dass alle Änderungen am GCO nach der Übergabe an die Methode addPropertyIndicator() nicht übernommen werden, bis man die Methode erneut aufruft und unter dem gleichen Namen die Instanz erneut übergibt:
PHP-Code
$criterion1 = new GenericCriterionObject(); $criterion2 = new GenericCriterionObject(); $criterion2->setLogicalOperator('OR') ->addPropertyIndicator('feld2','wert2') ->addPropertyIndicator('feld3','wert3') ->addPropertyIndicator('feld4','wert4'); $criterion1->addPropertyIndicator('feld1','wert1') ->addPropertyIndicator('feld2+feld3+feld4',$criterion2); // Änderung am GCO-Property-Wert für Feld 'feld4' $criterion2->addPropertyIndicator('feld4','wert4a'); $criterion1->addPropertyIndicator('feld1','wert1') // Überschreiben des Wertes für 'feld2+feld3+feld4' ->addPropertyIndicator('feld2+feld3+feld4',$criterion2);

5.3. Vergleichsoperator

Die Klasse GenericCriterionObject bietet die Möglichkeit den Vergleichsoperator zu verändern. Bisher wurden alle Vergleiche immer mit "=" angestellt. Um dies zu ändern wurde der Methode addPropertyIndicator() ein optionaler dritter Parameter hinzugefügt, über den der Vergleichsoperator gesetzt werden kann.

PHP-Code
$criterion = new GenericCriterionObject(); $criterion->addPropertyIndicator('feld1',15,'<');

Das obige Beispiel zeigt, wie man einen Vergleich kleiner als nutzen kann. Nach obiger Maßgabe können alle Vergleichsoperatoren verwendet werden, die für die konfigurierte Datenbank-Schnittstelle zur Verfügung stehen.

Der übergebene Vergleichsoperator wird nicht auf Validität überprüft! Der Programmierer hat dafür Sorge zu tragen, dass wirklich nur die Operatoren übergeben werden, die eine Datenbank verarbeiten kann, andernfalls wird beim Versuch den SQL-Befehl auszuführen eine Exceptions geworfen!

6. Erweiterung des Mapping- und Relation-Table

Wenn der GenericORRelationMapper über mehrere Anwendungen und mehrere Anwendungsfälle hinweg eingesetzt wird, ergibt sich die Schwierigkeit, dass unterschiedliche Applikationen unterschiedliche Bereiche der vom O/R-Mapper verwalteten Datenbank nutzen. Hierzu kann entweder für den entsprechenden Anwendungsfall jeweils eine passende Konfiguration angelegt werden oder der Entwickler definiert eine für alle verwendbare Basis-Konfiguration (z.B. alle Objekte des Moduls usermanagement) und nutzt die Methoden

  • addMappingConfiguration()
  • addRelationConfiguration()

um die allgemeingültige Konfiguration für den aktuellen Anwendungsfall zu erweitern. Mit den genannten Funktionen können beliebige weitere Objektdefinitions- und Beziehungs-Konfigurationen hinzugeladen werden. Das folgende Beispiel zeigt, wie die aufgeführten Methoden genutzt werden können um den Wirkungsbereich des Mappers zu erweitern:

PHP-Code
use APF\modules\genericormapper\data\GenericORRelationMapper; /* @var $ORM GenericORRelationMapper */ $ORM = & $this->getDIServiceObject('VENDOR\data\mapper', 'OR-Mapper'); // Zusaetzliche Objekt-Definitionen hinzuladen $ORM->addMappingConfiguration('APF\modules\usermanagement', 'umgt_2'); // Zusaetzliche Beziehungs-Definitionen hinzuladen $ORM->addRelationConfiguration('APF\modules\usermanagement', 'umgt_2');

Die Syntax der Objektdefinitions- und Beziehungs-Konfigurationen ist dabei identisch zu den Standard-Konfiguration, wie sie im Kapitel 2.3. Objekt- und-Beziehungsdefinition diskutiert wurden. Die zusätzliche Objekt-Definition beinhaltete dabei die folgenden Objekte:

APF-Konfiguration
[Project] DisplayName = "VARCHAR(100)" Description = "TEXT" [News] DisplayName = "VARCHAR(100)" Title = "VARCHAR(100)" Content = "TEXT"
und die neu hinzugekommenen Beziehungen waren
APF-Konfiguration
[Application2Project] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Project" [Project2News] Type = "COMPOSITION" SourceObject = "Project" TargetObject = "News"

7. Eigene Domänen-Objekte

Um einen zusätzlichen Schritt zur Objektorientierung zu gehen, unterstützt der genericormapper die optionale Möglichkeit, eigene Domänen-Objekte zu verwenden. Durch eine zusätzliche Konfigurationsdatei kann festgelegt werden, welche Objekttypen (respektive Datenbanktabellen) statt durch das GenericDomainObject durch ein eigenes Domänen-Objekt abgebildet werden sollen, und wo dieses Objekt zu finden ist.

Hierdurch kann man dem Domänen-Objekt eigene, objektspezifische Methoden spendieren, um Funktionen, die man früher über einen eigenen Manager abbilden musste, direkt im Objekt verankern zu können. Dies ist vorteilhafter für die Lesbarkeit des Codes, und vereinfacht die Anwendung in vielen Fällen merklich.

Mitgeliefert wird außerdem ein GenericORMapperDomainObjectGenerator, welcher in der Lage ist anhand der Konfigurationsdateien des genericormapper ein fertiges Grundgerüst für ein Domänen-Objekt an der konfigurierten Stelle zu erzeugen. Das Grundgerüst besteht zum einen aus einem Basisobjekt, welches für jede in der Datenbank konfigurierte Eigenschaft eine Getter- und Setter-Methode beinhaltet, und zwingend vom GenericDomainObject erben muss oder von einem anderen Objekt, welches von diesem erbt. Dieses Basisobjekt kann durch die Konfiguration bestimmt werden, sodass nach korrekter Konfiguration und Verwendung des GenericORMapperDomainObjectGenerator im einfachsten Fall keine weiteren Anpassungen notwendig sind. Desweiteren beinaltet das Grundgerüst das eigentliche Gerüst des Domänen-Objekts, welches von seinem jeweiligen Basisobjekt erbt.

Das Basisobjekt darf NICHT bearbeitet werden, da Änderungen an diesem durch erneutes ausführen des Generators (welcher gleichzeitig ein Updater darstellt, um beispielsweise neue Eigenschaften in die API aufzunehmen) unwiederruflich gelöscht werden. Für Änderungen ist das eigentliche Domänen-Objekt zur Verfügung, dieses wird bei einem Updatevorgang nicht mehr bearbeitet.

Als weiteres Feature kann das Domänen Objekt ein paar bestimmte "Event-Funktionen" implementieren, welche der GORM bei bestimmten Aktionen bei der Arbeit mit dem Objekt aufruft, beispielsweise vor und nach der Speicherung eines Objekts. Dies kann beispielsweise verwendet werden, um Daten bei der Verwendung des Domänen-Objekts als Array/Objekt bereitzuhalten, diese vor der Speicherung in das besser speicherbare JSON-Format umzuwandeln, und um nach der Speicherung wieder das Array/Objekt zur Verfügung zu stellen. Somit muss sich die Anwendung nichtmehr um die Umwandlung der Daten in das benötigte Format kümmern, dies kann jetzt das Domänen-Objekt automatisch erledigen.
Eine Übersicht der verfügbaren Event-Methoden finden Sie im Kapitel Event Methoden

Die folgenden Kapitel zeigen, wie eigene Domänen-Objekte konfiguriert, generiert und verwendet werden können, und welche Events sie unterstützen.

7.1. Konfiguration der Domänen-Objekte

Wenn der O/R-Mapper Domänen-Objekte verwenden soll, muss am selben Speicherort der Mapper-Konfigurationen noch eine zusätzliche Konfigurationsdatei angelegt werden:

Code
{ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini

Die Platzhalter {ENVIRONMENT} und {NAMEAFFIX} werden einfach an die bereits bestehenden Konfigurationsdateien des O/R-Mapper angeglichen.

In dieser Konfigurationsdatei wird für jedes Objekt, welches durch ein eigenes Domänen-Objekt abgebildet werden soll, eine Sektion mit dem Name des Objektes, welcher in der *_objects.ini des O/R-Mappers definiert ist, angelegt. Objekte, die kein eigenes Domänen-Objekt benötigen, brauchen nicht konfiguriert werden, für diese wird automatisch das GenericDomainObject verwendet.

Jede Sektion benötigt hier zwingend folgende Werte:

  • Class: Definiert den voll-qualifizierten Klassen-Namen des Objektes

Falls ein Basisobjekt nicht direkt vom GenericDomainObject, sondern von einem anderen Objekt (welches wiederum vom GenericDomainObject erbt) erben soll, und der Generator zum Erzeugen des Grundgerüstes verwendet werden soll, müssen noch folgende, ansonsten nicht benötigte, Werte definiert werden:

  • Base.Class: Der voll-qualifizierten Klassen-Namen des zu verwendenden Basisobjektes

Nachfolgendes Beispiel ist ein Teil der Konfiguration der Postbox Extension, in welcher die Domänen-Objekte als erstes verwendet wurden, und welche daher auch als weiterführendes Beispiel für die Verwendung herangezogen werden kann.

APF-Konfiguration
[Message] Class = "APF\extensions\postbox\biz\Message" Base.Class = "APF\extensions\postbox\bizAbstractMessage" [MessageChannel] Class = "APF\extensions\postbox\bizMessageChannel" Base.Class = "APF\extensions\postbox\bizAbstractMessageChannel"

Definiert wurden hier 2 Objekte, welche beide von einem speziellen Basisobjekt erben sollen.

7.2. Generierung der Domänen-Objekte

Mithilfe des mitgelieferten GenericORMapperDomainObjectGenerator gestaltet sich das Erstellen der Domänen-Objekte sehr einfach. Sobald Sie nach obigem Schema die Konfigurationen angelegt haben, muss nurnoch ein kleines Script zur Generierung der Objekte angelegt und ausgeführt werden:

PHP-Code
// Einbinden der APF Bootstrap-Datei require('./APF/core/bootstrap.php'); // Umgebung konfigurieren (wichtig für das Laden von Konfigurationen) use APF\core\registry\Registry; Registry::register('APF\core', 'Environment', '{ENVIRONMENT}'); use APF\modules\genericormapper\data\tools\GenericORMapperDomainObjectGenerator; $generator = new GenericORMapperDomainObjectGenerator(); // Context definieren (wichtig für das Laden von Konfigurationen) $generator->setContext('{CONTEXT}'); // Initialisiert die Mapping-Konfiguration $generator->addMappingConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); // Initialisiert die Beziehungs-Konfiguration $generator->addDomainObjectsConfiguration('{CONFIG_NAMESPACE}', '{CONFIG_NAME_AFFIX}'); $generator->generateServiceObjects();

Der Platzhalter {CONTEXT} muss durch ihren Context ersetzt werden, {NAMESPACE} durch den Namespace unter dem die Konfigurationsdatei abgelegt wurde (ohne Context) und {NAMEAFFIX} durch den im Dateinamen definierten Affix (siehe oben).

Der GenericORMapperDomainObjectGenerator wird beim Aufruf dieses Scriptes für jedes definierte Objekt ein entsprechendes Domänen-Objekt am, in der Konfiguration definierten, Speicherort erzeugen. Sollte dort bereits eine Datei mit dem selben Namen gefunden werden, wird versucht das darin enthaltene Basisobjekt neu zu generieren, um API-Änderungen in der *_objects.ini zu übernehmen. Hierfür sind in der erzeugten Datei bestimmte, entsprechend gekennzeichnete, Kommentare enthalten, die auf keinen Fall entfernt oder geändert werden dürfen, genausowenig wie alles zwischen diesen Kommentaren, da sonst Datenverlust droht! Beim Update wird das Domänen-Objekt selber nichtmehr geändert, Änderungen an diesem sollten nicht verloren gehen. Alle Änderungen am Basisobjekt werden unwiederruflich und ohne Warnung verworfen.

Das APF-Team übernimmt keine Haftung für Fehler durch die automatische Generierung, trotz sorgfältiger Prüfung können Fehler immer unerwartet auftreten, daher ist eine vorherige Sicherung der bereits bestehenden Dateien anzuraten, um diese im Fehlerfall wiederherstellen zu können!

Ein Beispiel für eine der erzeugten Dateien (hier für das Message-Objekt aus obiger Definition) könnte in etwa so aussehen:

PHP-Code
namespace APF\extensions\postbox\biz; //<*MessageBase:start*> DO NOT CHANGE THIS COMMENT! /** * Automatically generated BaseObject for Message. !!DO NOT CHANGE THIS BASE-CLASS!! * CHANGES WILL BE OVERWRITTEN WHEN UPDATING!! * You can change class "Message" which will extend this base-class. */ use APF\extensions\postbox\biz\AbstractMessage; class MessageBase extends AbstractMessage { public function __construct($objectName = null) { parent::__construct('Message'); } public function getText() { return $this->getProperty('Text'); } public function setText($value) { $this->setProperty('Text', $value); return $this; } public function getAuthorNameFallback() { return $this->getProperty('AuthorNameFallback'); } public function setAuthorNameFallback($value) { $this->setProperty('AuthorNameFallback', $value); return $this; } } // DO NOT CHANGE THIS COMMENT! <*MessageBase:end*> /** * Domain object for "Message" * Use this class to add your own functions. */ class Message extends MessageBase { /** * Call parent's function because the objectName needs to be set. */ public function __construct($objectName = null) { parent::__construct(); } }

Das Basisobjeckt MessageBase erbt vom, explizit in der Konfiguration definierten, Objekt AbstractMessage. Auch um den Import der benötigten Datei hat der Generator sich natürlich selbst gekümmert.
In der *_objects.ini des O/R-Mapper wurden dem Message-Objekt die Eigenschaften "Text" und "AuthorNameFallback" gegeben, dementsprechend wurden für diese beiden Eigenschaften Getter- und Setter-Methoden generiert. Die Setter-Methoden erhalten dabei immer automatisch ein Fluent-Interface.
Zuletzt die Definition des Message-Objektes, welches vom Basisobjekt erbt. In dieser Klasse können sie nun Ihre eigenen Funktionen definieren, denkbar wäre in diesem Fall z.B. die Funktion "delete()" um die aktuelle Nachricht zu löschen.

Sollten sie sich die Postbox-Extension einmal näher angesehen haben, werden sie feststellen, dass dort die delete-Methode bereits in AbstractMessage definiert wurde. Auch dies ist problemlos möglich, und im Falle der Postbox angewendet worden, im Normalfall aber nicht notwendig.

7.3. Verwendung der Domänen-Objekte

Wenn die Objekte entsprechend obiger Anleitung angelegt wurden, kann es nun zur praktischen Verwendung gehen. Hierbei gibt es eigentlich nichts zu beachten, da die Objekte immer zwingend vom GenericDomainObject (direkt oder indirekt durch Weitervererbung ist egal) erben, können Sie auch wie jedes normale GenericDomainObject verwendet werden und sind somit abwärtskompatibel. Dies ist besonders praktisch, da eine nachträgliche Verwendung der eigenen Domänen-Objekte keinerlei Anpassung ihrer bereits vorhanden Codeteile benötigt.

Der O/R-Mapper erkennt beim Laden von Daten automatisch anhand der Konfiguration, dass er ein spezielles Domänen-Objekt verwenden muss, und erzeugt dieses anstatt des GenericDomainObject. Laden Sie also ab sofort ein Message-Objekt aus der Datenbank, erhalten Sie ein Objekt vom Typ "Message", auf welches sie ihre eigenen Funktionen anwenden können.

Um ein neues Objekt zu erzeugen, binden Sie diese bitte mittels use ein um danach eine Instanz des Objektes zu erzeugen. Anschließend können Sie wie gewohnt damit arbeiten:

PHP-Code
use APF\extensions\postbox\biz\Message; $message = new Message(); $message->setText('ExampleText'); $orm->saveObject($message);

7.4. Event Methoden

Wie bereits angesprochen beherrscht der O/R-Mapper auch ein paar "Events". Die Domänen-Objekte können für jedes Event eine Event-Methode definieren, welche bei entsprechender Aktion aufgerufen wird. Zur Verfügung stehen derzeit folgende Methoden, welche der O/R-Mapper aufruft:

  • afterLoad(): Wird nach dem Laden des Objektes aus der Datenbank aufgerufen.
  • beforeSave(): Wird direkt vor dem Speichern des Objektes in die Datenbank aufgerufen.
  • afterSave(): Wird direkt nach dem Speichern des Objektes in die Datenbank aufgerufen.

Ein mögliches Anwendungsbeispiel wäre das bereits in der Einleitung erwähnte kodieren und dekodieren von Arrays oder Objekten vor bzw. nach dem Speichern oder Laden.

7.5. Objekt-Bäume

Der Generische O/R-Mapper bietet einen Mechanismus, mittels dem sich hierarchische Objekt-Listen - also Objekt-Bäume - erstellen lassen. Dieses Feature ist vergleichbar mit dem Nested-Sets- oder ParentID-Prinzip.

Um dieses Feature nutzen zu können, muss in der Datei {ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini ein Domain-Objekt definiert sein, welches als Basis-Klasse die Klasse TreeItem verwendet oder aber man definiert direkt die Klasse TreeItem als Domain-Object:

APF-Konfiguration
; Option 1 [NavigationNode] Class = "VENDOR\..\NavigationNode" Base.Class = "APF\modules\genericormapper\data\TreeItem" ; Option 2 [NavigationNode] Class = "APF\modules\genericormapper\data\TreeItem"

Die Hierarchie der Objekte muss über eine Kompositions-Tabelle abgebildet werden:

APF-Konfiguration
[NavigationNode2NavigationNode] Type = "COMPOSITION" SourceObject = "NavigationNode" TargetObject = "NavigationNode"

Einen Objekt-Baum erhält man nun durch den Aufrauf der Methode loadObjectTree() des GenericORRelationMapper:

PHP-Code
$tree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode');

Diese Methode kennt noch 3 weitere, optionale Parameter:

  • criterion: Hier kann ein GenericCriterionObject übergeben werden, um die Abfrage der Objekte, die für den Baum verwendet werden, zu beinflussen.
  • rootObjectId: Wird hier eine ID eine Objektes übergeben, wird dieses Objekt als Wurzel-Objekt für den Baum verwendet.
  • maxDepth: Mit diesem Parameter lässt sich die Tiefe des Baumes begrenzen.

Sobald ein Objekt-Baum erstellt/abgefragt wurde, ist es möglich über zwei Methoden, welche in der Klasse TreeItem definiert sind, die Kind- bzw. das Eltern-Objekt des aktuellen Tree-Items abzufragen. Über Rekursion wäre es nun möglich, den Objekt-Baum bspw. mittels einer verschachtelten ungeordneten Liste auszuzeichen:

PHP-Code
function printChildObjects($objects) { echo '<ul>'; foreach ($objects as $object) { echo '<li>'; echo $object->getProperty('DisplayName'); $children = $object->getChildren(); if (count($children) > 0) { printChildObjects($children); } echo '</li>'; } echo '</ul>'; } $objectTree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode'); printChildObjects($objectTree);

8. Performance-Tricks

Der O/R-Mapper ist grundsätzlich auf maximale Performance ausgelegt, jedoch kann dieser durch ungünstige Implementierung der darüberliegenden Schichten oder durch nicht optimale Definition der Tabellen negativ beeinflusst werden.

Das vorliegende Kapitel zeigt Themen auf, bei denen ein manueller Eingriff in die Mapperschicht oder die Verwendung der Mapper-Schicht positive Performance-Effekte hat.

8.1. Häufige Abfrage über Attribute

Werden Abfragen häufig über Attribute eines Objekts definiert bzw. eingeschränkt, so ist es ratsam, einen Index auf die verwendete Spalte oder die verwendeten Spalten zu legen.

Zusätzliche Indides lassen sich sehr einfach in den {ENVIRONMENT}_{NAMEAFFIX}_objects.ini anlegen. Beachten Sie hierzu bitte die Hinweise in Kapitel 2.4. Es ist natürlich ebenso möglich, Indizes manuell anzulegen.

Es wird empfohlen, die zusätzlich notwendigen Indizes über die Objekt-Definition zu realisieren, da diese beim automatisierten Anlegen (siehe Kapitel 3.1) und Aktualisieren (siehe Kapitel 3.2) der Datenbank automatisch erzeugt oder aktualisiert werden.

8.2. Erzeugen der Mapperschicht

Den Aufbau der internen Mapping- und Beziehungs-Tabelle gehört neben dem Aufbau der Datenbank-Verbindung zu den teuersten Operationen. Setzen Sie daher bei der Erzeugung des GenericORRelationMapper auf den Erzeugungsmodus SESSIONSINGLETON oder APPLICATIONSINGLETON (Details siehe Erzeugung von Objekten). Dies steigert die Performance und spart rund 20% pro Aufruf.

Um den Mapper zu erzeugen nutzen Sie bitte die zweite Option der APFObject::getServiceObject()-Methode:

PHP-Code
$ormFact = &$this->getServiceObject( 'APF\modules\genericormapper\data\GenericORMapperFactory', APFService::SERVICE_TYPE_SESSION_SINGLETON ); $orm = &$ormFact->getGenericORMapper(..., ...);

Bei der Erzeugung über den DIServiceManager setzen sie das Attribut servicetype bitte auf den gewünschten Wert. Beispiel:

APF-Konfiguration
[OR-Mapper] servicetype = "SESSIONSINGLETON" class = "APF\modules\genericormapper\data\GenericORRelationMapper" ...

8.3. JOIN-Optimierung

Das Ausführen von vielen JOIN-Operationen pro Statement kann bei ungünstiger Definition von Indizes zu einer deutlichen Verlängerung der Ausführungszeiten führen. Bei großen Datenmengen oder komplizierten Abfragen lohnt es sich daher, JOIN-Abfragen selbst zu schreiben und mit Hilfe der Methode load*ByStatement() auszuführen.

Bitte beachten Sie hier jedoch, die am stärksten einschränkenden JOIN-Statements zuerst zu platzieren um die anschließend verarbeitete Datenmenge bestmöglich zu optimieren.

Bitte beachten Sie, dass der O/R-Mapper bereits für jede Beziehung JOIN-Indizes anlegt. Beispiel:
SQL-Statement
CREATE TABLE `cmp_application2role` ( `Source_ApplicationID` int(5) unsigned NOT NULL DEFAULT '0', `Target_RoleID` int(5) unsigned NOT NULL DEFAULT '0', KEY `JOIN` (`Source_ApplicationID` , `Target_RoleID`), KEY `REVERSEJOIN` (`Target_RoleID` , `Source_ApplicationID`) );
Es ist daher nur in Ausnahmefällen ratsam, eigene bzw. weitere Indizes zur Optimierung der Laufzeit von Statements anzulegen!

9. Anmerkungen

Die Quellcode-Dateien des Usermanagement-Modul-Moduls können als weiterführende Beispiele herangezogen werden.

Ein weiterführendes Tutorial für den Einstieg finden Sie zudem im Wiki.

Kommentare

Möchten Sie den Artikel eine Anmerkung hinzufügen, oder haben Sie ergänzende Hinweise? Dann können Sie diese hier einfügen. Die bereits verfassten Anmerkungen und Kommentare finden Sie in der untenstehenden Liste.
« 1   »
Einträge/Seite: | 5 | 10 | 15 | 20 |
1
Christian 09.10.2008, 12:47:44
Der Thread http://forum.adventure-php-framework.org/de/viewtopic.php?f=5&t=54 im Forum beinhaltet auch einige interessante Punkte zum GenericORMapper.