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 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.
Seit dem Release 1.11 unterstützt der Generic O/R-Mapper 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.

Seit 1.14 können auch Selbstreferenzen angelegt werden, 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

Mit dem Release 1.12 ist es 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

Mit dem Release 1.16 ist es 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.

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 /apps/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
// Page-Controller einbinden require('../../apps/core/pagecontroller/pagecontroller.php'); // Umgebung konfigurieren (wichtig für das Laden von Konfigurationen) Registry::register('apf::core','Environment','{ENVIRONMENT}'); import('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

Seit dem Release 1.11 ist ein Update-Tool für den O/R-Mapper verfügbar, das die Änderungen an den Konfigurations-Dateien auf eine vorhandene Datenbank anwendet.

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-Tool besitzt derzeit noch die Einschränkung, dass bei einigen MySQL-Versionen Spalten mit DEFAULT-Werten nochmals mit einem Update-Statement belegt werden, ob wohl 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 Update-Tools 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 /apps/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
// Page-Controller einbinden require('../../apps/core/pagecontroller/pagecontroller.php'); // Umgebung konfigurieren (wichtig für das Laden von Konfigurationen) Registry::register('apf::core','Environment','{ENVIRONMENT}'); import('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 (siehe Kapitel Performance-Hacks. 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( '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.

Seit dem Release 1.12 definiert die Factory den Service-Typ des Mappers (=Gültigkeitsbereich des Objekts). 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üt 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

Seit dem Release 1.12 kann der GORM auch mit dem DIServiceManager erzeugt werden. Diese Vorgehensweise hat 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.

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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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','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.

Mit dem Release 1.12 wurde die Klasse GenericDomainObject um einige Methoden zur Vereinfachung der Implementierung erweitert. Ü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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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','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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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 '<br />'.$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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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 '<br />'.$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
// Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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
$ormf = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); $orm = &$ormf->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); $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
// Fabric instanziieren $ormf = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $orm = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // Benutzer befuellen $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // Benutzer speichern $orm->saveObject($user);
Ab dem Release 1.11 kann das Objekt (in diesem Fall $user) 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
// Fabric instanziieren $ormf = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $orm = &$ormf->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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
class UsermanagementManager extends APFObject { public function getUserList(){ // Fabric instanziieren $ORMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // 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.
Seit dem Release 1.13 bietet die Klasse GenericCriterionObject 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

Seit dem Release 1.14 bietet die Klasse GenericCriterionObject 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 in Version 1.13 eingeführte 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

Seit dem Release 1.14 bietet die Klasse GenericCriterionObject 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
// Fabric instanziieren $oRMF = &$this->getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // Mapper mit Basis-Konfiguration laden $oRM = &$oRMF->getGenericORMapper('modules::usermanagement','umgt_1','umgt'); // Zusaetzliche Objekt-Definitionen hinzuladen $oRM->addMappingConfiguration('modules::usermanagement','umgt_2'); // Zusaetzliche Beziehungs-Definitionen hinzuladen $oRM->addRelationConfiguration('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. Anmerkungen

Hinweise zur Performance können unter Performance-Hacks nachgelesen werden. Die Quellcode-Dateien des Usermanagement-Modul-Moduls können als weiterführende Beispiele herangezogen werden.

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.