Generic OR mapper

1. Introduction

Designing object oriented applications, you typically claim, that the complete design can be created on OO style - without break of media. In order to achieve this goal, each developer was yet faced with the problem of data storage in relation databases. If there is no tool, a DataMapper has to be implemented newly for each application. This does not only result in higher costs and lack of time, but is also not compatible with the DRY (don't repeat yourself) paradigma. You still are forced to produce redundant code.

The APF module genericormapper now provides an abstraction layer, that is intended to do most of the work a data mapper has to do. The functionality thereby can
  • manage objects,
  • manage relations between objects (compositions and assoziations) and
  • features CRUD functionality for objects and object structures (aka. trees).
For this tasks, the API features a couple of methods, that support loading, manipulation and deletion of objects within the database. The generic domain object (GenericDomainObject) returned by the API functions can on the one hand be used directly within your applications or your application features a simple data layer, that mapps the data mapper domain objects into your application's domain objects and vice versa.

The following chapters show how the OR mapper has to be configured and how you can use it. The module usermanagement shipped with the APF release is completely based on the GenericORMapper and thus can be used as an extended implementation example. Details on the module can be seen on the Usermanagement page.


2. Configuration of the OR mapper


2.1. Basics

To be able to user the OR mapper, two configuration files must be present:
  • {ENVIRONMENT}_{NAMEAFFIX}_objects.ini
  • {ENVIRONMENT}_{NAMEAFFIX}_relations.ini
The first file defines the objects and attributes, the second one is intended to define the relations between the objects from the first file. Due to the fact, that the GenericORRelationMapper uses the ConnectionManager for database access, a database connection configuration must be created if not done yet.

The {ENVIRONMENT} part of the configuration file names is taken from the registry directive Environment from the apf::core namespace. The {NAMEAFFIX} can be defined freely by the developer. This part of the name is inteded to provide the possibility of different mapper configurations for one context and one environment. This enables you to create applications, that are able to use multiple data sources.


2.2. Configuration examples

Imagine, that a developer wants to create a guestbook. The source code files are located in the modules::myguestbook namespace and the guestbook's data layer should us the OR mapper. Further, the registry value Environment is not touched (so it is still the default value) and the current application context is sites::mysite. Moreover, he decides, that the name affix should be identical to guestbook. In this case, the object configuration file must be named
Code
DEFAULT_guestbook_objects.ini
and the relation configuration file
Code
DEFAULT_guestbook_relations.ini
These must reside within the folder
Code
/apps/config/modules/myguestbook/sites/mysite
Further details on configuration, namespaces and contexts can be taken from the configuration section.


2.3. Object and relation definition

The way of object and relation definition is described in the next two chapters:

2.3.1. Object definition
The GenericORRelationMapper provides a generic domain object (GenericDomainObject) that represents a persistence object. The type of the concrete object is classified by the domain object's ObjectName property.

The object definition thus contains the name of the object (=name of the section) and the attributes (=properties of the GenericDomainObject class). The following codebox shows a typical object configuration:
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)"
The values of the attributes represent the database data types of the object's properties. The mapper knows the following values and translates them into the right create table statements:
  • VARCHAR({LENGTH})
  • TEXT
  • DATE
The {LENGTH} place holder must be replaced by any number. All other values are directly used for the data type of a property. These must be valid data type definitions. Otherwise, the CREATE TABLE statements will fail. The values presented above should be suitable for most application cases.
Since release 1.11, the generic or mapper supports bit masking. For this reason, the value of the property definition must contain a valid BIT field definition like
Code
bit(7) NOT NULL default b'0'
Therby, it is not relevant, if the property is defined with a default value. But it is important that the declaration of the BIT field ocntains the keyword "BIT". Details can be taken from the forum discussion under Fehler mit BIT-Feldern (German).
The attributes of a domain object can then be addressed as follows:
PHP-Code
... $User = new GenericDomainObject('User'); $User->setProperty('FirstName','Christian'); $User->setProperty('LastName','Achatz'); ... echo 'Surename: '.$User->getProperty('FirstName'); echo 'Name: '.$User->getProperty('LastName'); ...

2.3.2. Relation definition
The file *_relations.ini defines the relations between the objects defined in the previous chapter. The mapper knows two types of relations: compositions and associations. Due to the fact, that compositions are strong relations, objects, that compose other objects, cannot be deleted due to data consistency. This case is checked by the mapper and it throws an error, if a object is tried to be deleted, that must not.

Note: The persistancy theory contains the rule, that each object should be composed exactly one time. This is because an object cannot have more than one strong binding to another object in real life, too. Futhermore, a composition relation defines the object's right to exist. Hence, be aware that your objects are composed concerning this rule. For example, a guestbook entry cannot exist without the guestbook object. Instead, a user can exist without the guestbook. This means, that the relation between a guestbook and an entry must be a composition, the relation of the entry to the user must be an association.

The following codebox shows a typical relation configuration:
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"
The section name (e.g. Group2User) should be named self-explanatory, because this relation key can be used to load objects, that are related to others and to create a relation between two or more objects. The type attribute contains the quality of relation, the SourceObject and TargetObject params reference the relevant object definition sections of the object definition file.

Note:
  • The count of the relation definitions is not limited, each definition should be created to fit the requirements of the data model of the application. The rule of thumb here says, that when using one attribute more than one time in different objects, it should be outsourced to an independent object and referenced (associated) by the relevant objects. A typical example is the language.
  • If you have created an object tree using the method addRelatedObject(), you can read the related objects of the desired node (GenericDomainObject) within the tree using getRelatedObjects() for futher manipulation or processing.

2.4. Additional indices

Since version 1.12 it is possible to define additional indices along with the objct definition. This may be necessary due to performance reasons and it is strongly recommended to use this feature, if you are heavily requesting data using index restriction.

In such cases an object definition can be added the AddIndices key. The information provided there is used during setup and update to create additional indices to speed up the query execution time.

The subsequent code box describes a simple example with three types of indices defined within a user's object definition:

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

The additional index definition underlies the following scheme:

  • Each additional index is defined by the included properties and the index type. Multiple definitions are separated by "|" (Pipe).
  • Valid index types are: INDEX (normal index), UNIQUE (property may only contain a unique value), And FULLTEXT (search index, that can be searched using a MATCH AGAINST() statement). All index types are noted in brackets.
  • In case multiple properties should be included in one index definition, the properties must be separated by colon (","). The names of the properties must be equal to the names used for the usual object definition.
Details on the discussion of this feature can be taken from the wiki page Zusätzliche Indizes für Setup-/Update-Tool GORM (German) and the forum thead Zusätzliche Indizes für Setup-/Update-Tool GORM (German).

3. Database setup

After the configuration files have been created, the database must be prepared for usage. This can be done manually or automatically. The manual way is described in the manual database setup chapter.

The following script shows, how the automated database setup can be done with aim of the GenericORMapperSetup tool. If you are searching for an example script, have a look at the /apps/modules/genericormapper/data/tools folder in the adventure-codepack-* release. The file is named setup.php and must be adapted to your application case:
PHP-Code
// include page controller require('../../apps/core/pagecontroller/pagecontroller.php'); // configure the registry if desired Registry::register('apf::core','Environment','{ENVIRONMENT}'); // include SetupMapper import('modules::genericormapper::data::tools','GenericORMapperSetup'); // create setup tool $setup = new GenericORMapperSetup(); // set Context (important for the configuration files!) $setup->setContext('{CONTEXT}'); // adapt storage engine (default is MyISAM) $setup->setStorageEngine('MyISAM|INNODB'); // adapt data type of the indexed columns, that are used for object and relation ids //$setup->setIndexColumnDataType('INT(5) UNSIGNED'); // create database layout $setup->setupDatabase('{CONFIG_NAMESPACE}','{CONFIG_NAME_AFFIX}','{CONNECTION_NAME}'); // display statements only $setup->setupDatabase('{CONFIG_NAMESPACE}','{CONFIG_NAME_AFFIX}');
The place holders within the script have the following meaning:
  • {ENVIRONMENT}: This is the environment indicator of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the configuration chapter.

  • {CONTEXT}: This is the context of the application. It is used for addressing configuration files and must be adopted, if your environment is set to a value different than the default one. For details have a look at the configuration chapter.

  • {CONFIG_NAMESPACE}: The namespace, that contains the mapper configuration files (see chapter 2.2).

  • {CONFIG_NAME_AFFIX}: The name affix for the configuration files (see chapter 2.1).

  • {CONNECTION_NAME}: The name of the database connection, that is used for the setup and for productional use.

Further, it is important that the database, that should be initialized, must exist before setup is started. Additionally, the user connecting to the database must have CREATE TABLE rights. Otherwise, the setup could not be done. If no error is displayed during setup, the setup has finished successfully. The result can be checked by using phpMyAdmin or a MySQLAdmin tool.

Executing the script to display the create statements only, the output should look like this:

Code
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, `ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `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, `ApplicationID` INT(5) UNSIGNED NOT NULL default '0', `RoleID` INT(5) UNSIGNED NOT NULL default '0', PRIMARY KEY (`CMPID`), KEY `JOININDEX` (`ApplicationID`,`RoleID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Invoking setIndexColumnDataType() the data type of the columns that store the object and relation ids can be adapted. Concerning the sample statements above, the type was changed to INT(5) UNSIGNED. In case you need smaller or greater data rooms, you can adjust it using
PHP-Code
$setup->setIndexColumnDataType('TINYINT(3)');
Please be aware, that the value set with this method is directly used within the create statements. In case you are using invalid data types, this will lead to errors during setup of the tables.

Using phpMyAdmin you will get the following screen:

PHPMYAdmin view

Now, the configuration of the mapper has finished and you can use it. Changes to the data model (at the moment) cannot be automatically applied to the database. For this reason, the affected tables must be altered manually. Due to the fact, that the database layout follows some easy ruls, you can easily do this yourself. Hints can be taken from the manual database setup chapter.

Supporting the task of updating a database setup with the generic OR mapper setup tool a update tool was introduced in release 1.11. For detailed description, please refer to Generic O/R mapper - database update.

4. Usage of the OR mapper

The OR mapper, more precisely the GenericORRelationMapper, offers a list of API methods, that can be used for data and relation usage and manipulation. The following list gives you an overview of the existing functions and their meaning:
  • loadObjectListByCriterion(): Loads an object list by a criterion object.

  • loadObjectByCriterion(): Loads an object by a criterion object.

  • loadRelatedObjects(): Loads a list of objects, that are related to a dedicated object, by a relation key.

  • loadNotRelatedObjects(): Loads a list of objects by a relation key, that are *not* related to a dedicated object.

  • loadRelationMultiplicity(): Returns the number of objects, that are related to a dedicated object.

  • saveObject(): Saves an object or object tree, that consists of objects related to each other using the defined relations.

  • deleteObject(): Deletes an object. Associations and compositions are deleted as well.

  • createAssociation(): Creates an association between two objects.

  • deleteAssociation(): Deletes the associations between two objects.

  • isAssociated(): Checks, if two objects are associated.

  • loadObjectListByStatement(): Loads a list of objects by a statement.

  • loadObjectListByTextStatement(): Loads a list of objects by a statement present as a string.

  • loadObjectListByIDs(): Loads a list of objects by a list of ids.

  • loadObjectByStatement(): Loads an object by a statement.

  • loadObjectByTextStatement(): Loads an object by a statement present as a string.

  • loadObjectByID(): Loads an object by a given id.

The *Statement* methods are provided due to performance optimization reasons. For details, please have a look at the performance hacks chapter. Details on the aruments can be taken from the API documentation page for the modules.

4.1. Creating an instance of the mapper

4.1.1. Classic approach

The instance of the OR mapper must be created using the GenericORMapperFactory. This is necessary, because the concrete mapper must be initialized prior to use and to have the possibility to use more than one instance per module. The latter one is probably not necessary within easy applications, but in complexe environments this may be a knock-out criterion.

The next code box shows a typical call of the mapper:

PHP-Code
// create the factory using the desired service mode $ormFact = &$this->__getServiceObject( 'modules::genericormapper::data', 'GenericORMapperFactory'[, {SERVICE_OBJECT_TYPE}] ); // create the mapper using the factory $orm = &$ormFact->getGenericORMapper( {CONFIG_NAMESPACE}, {CONFIG_NAME_AFFIX}, {CONNECTION_NAME}[, $logStatements = false] );

The place holders have the following meaning:

  • {SERVICE_OBJECT_TYPE}: The type of service object of the factory. This param implicitly defines service type of the mapper. Allowed values NORMAL, SINGLETON, and SESSIONSINGLETON. Default is SINGLETON. Details can be taken from the service object section.
  • {CONFIG_NAMESPACE}: The namespace, the configuration files reside (see chapter 2.2).
  • {CONFIG_NAME_AFFIX}: The name affix, the configuration files have (see chapter 2.1).
  • {CONNECTION_NAME}: The name of the database connection used for setup and usage.

As of release 1.12 the factory service type also defines the service type of the mapper (=scope of the object concerning the service manager's instanciation model). In case, the GORM should be created only once per session due to performance reasons (mapping and relation tables are then only created once per visit) the factory creation statement must have the third param (service type) filled with the value SESSIONSINGLETON.

For development environments it is recommended to use the SINGLETON service type. Using the SESSIONSINGLETON type, the mapping and relation tables are only updated after the session has expired what may lead to unexpected results.

Please note, that the factory must be created by the __getServiceObject() function to avoid unnecessary configuration side-effects.

In case, you want to acivate statement logging for debugging reasons, the $logStatements must be set to true. Please do not use this option in productional environments! Details can be taken from the API documentation.
4.1.2. Creation via DI

As of release 1.12 the GORM can also be created using the DIServiceManager. This approach implies advantages to testability and decoupling of business layer and presentation layer.

Creating an instance of the GORM is thus directly done by the DIServiceManager and not using the factory described above. This is because the DIServiceManager only accepts explicit service objects for dynamic initialization of other services. For this reason, three new services are available that do not contain any logic but the necessary configuration information:

  • GenericORMapperDIConfiguration: injection of the basic configuration
  • GenericORMapperDIMappingConfiguration: injection of additional mapping configurations
  • GenericORMapperDIRelationConfiguration: injection of additional relation configurations

Details in the configuration beside an application sample can be found on the wiki page Erzeugen des GORM mit dem DIServiceManager (German).

4.2. Loading data

To have a concrete example for the next code samples, the following UML diagram should be used. The picture contains the business object definition of the usermanagement module contained in the APF. The code presented here is thus taken from the module's code files.



4.2.1. Loading objects

To load objects, you can use the functions

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

If you intend to display the details of a user (see UML), you kan take the methods listed above to achieve this:

PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // load user (1) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('UserID',1); $User = $ORM->loadObjectByCriterion('User',$Crit); // load user (2) $select = 'SELECT * FROM ent_user WHERE UserID = \'1\';'; $User = $ORM->loadObjectByTextStatement('User',$select); // load user (3) $User = $ORM->loadObjectByStatement('User','modules::usermanagement','load_user_by_id'); // load user (4) $User = $ORM->loadObjectByID('User',1);

The content of the statement file load_user_by_id is:

Code
SELECT * FROM ent_user WHERE UserID = '1';

Details on the statement execution can be taken from the class reference table of the MySQLHandler.

As of release 1.12 GenericDomainObject was added seom convenience methods to ease implementation. By typing
PHP-Code
$user = $orm->loadObjectByID('User',1); echo $user->getObjectId();
you can easily retrieve the object's id without thinking about the id's property name. Using setObjectId() the object id can be set to the applied value. In cse you are interested in the object's name, just do the following:
PHP-Code
$user = $orm->loadObjectByID('User',1); echo $user->getObjectName();
Further, the GenericDomainObject now supports PHP's automatic string conversion. Thus a user object can be printed like this for debug reasons:
PHP-Code
$user = $orm->loadObjectByID('User',1); echo $user;
4.2.2. Loading lists
For loading lists, the OR mapper features the following methods:
  • loadObjectListByCriterion()
  • loadObjectListByTextStatement()
  • loadObjectListByStatement()
  • loadObjectListByIDs()
If you like to display a list of users (see UML) you can use these methods as presented below:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // load user list (1) $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('DisplayName','a%'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // load user list (2) $select = 'SELECT * FROM ent_user WHERE DisplayName LIKE \'a%\';'; $UserList = $ORM->loadObjectListByTextStatement('User',$select); // load user list (3) $UserList = $ORM->loadObjectListByStatement('User','modules::usermanagement','load_user_list'); // load user list (4) $UserList = $ORM->loadObjectListByIDs('User',array(1,2,3,4,5,6));
The content of the statement file load_user_list looks like this:
Code
SELECT * FROM ent_user WHERE DisplayName LIKE 'a%';

If you have to display the corresponding groups while listing the user details, you can use the
  • loadRelatedObjects()
function. This method returns the objects related to a dedicated object by the desired relation key. The next example shows, how to load the groups associated to a user:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // load user list $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $UserList = $ORM->loadObjectListByCriterion('User',$Crit); // display the list including the associated groups for($i = 0; $ < count($UserList); $i++){ // display name of the user echo $UserList[$i]->getProperty('DisplayName'); // load corresponding groups $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User'); // display groups echo ' ,Gruppen: '; for($j = 0; $j < count($GroupList); $j++){ echo $GroupList[$j]->getProperty('DisplayName').' '; // end for } // end for }
To ease the handling of loading related objects, the GenericDomainObject posesses the loadRelatedObjects() function. This enables you to load the related objects using a relation key, where no instance of the mapper is available. To apply this to the example above, the user's groups could be loaded using
PHP-Code
$GroupList = $UserList[$i]->loadRelatedObjects('Group2User');
Note: The amount of objects loaded, can be limited using the GenericCriterionObject. So if you like to load the first 10 groups only, include the following code in your application:
PHP-Code
// define the limit $Crit = new GenericCriterionObject(); $Crit->addOrderIndicator('DisplayName','ASC'); $Crit->addPropertyIndicator('DisplayName','A%'); $Crit->addCountIndicator(10); // load the group list using the domain object $GroupList = $UserList[$i]->loadRelatedObjects('Group2User',$Crit); // load the group list using the mapper $GroupList = $ORM->loadRelatedObjects($UserList[$i],'Group2User',$Crit);

Often you have to select objects, that are not (yet) related with a given object, but a relation is defined. A good example, concerning the UML diagram above, is the listing of groups, that a certain user has not been added yet. For this purpose, the
  • loadNotRelatedObjects()
function could be used. The next example shows, how groups not associated with desired user can be selected:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // select user $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // select groups, that are not related with the user $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User'); // present list for($i = 0; $ < count($GroupList); $i++){ echo '
'.$GroupList[$i]->getProperty('DisplayName'); // end for }
Note: The amount of objects loaded, can be limited using the GenericCriterionObject. A typical application case is to limit the list by relations to other objects. The following example explains, how to load all groups, that are not associated to the desired user but are composed under a Application object:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // select user $Crit = new GenericCriterionObject(); $Crit->addpropertyIndicator('DisplayName','Mustermann, Max'); $User = $ORM->loadObjectByCriterion('User',$Crit); // define additive relation criterion $Crit = new GenericCriterionObject(); $App = new GenericDomainObject('Application'); $App->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2Group',$App); // select groups, that are not related with the user $GroupList = $ORM->loadNotRelatedObjects($User,'Group2User',$Crit); // present list for($i = 0; $ < count($GroupList); $i++){ echo '
'.$GroupList[$i]->getProperty('DisplayName'); // end for }

4.2.5. Loading the relation multiplicity

To be able to find out, how much objects are related to a certain one, the
  • loadRelationMultiplicity()
method can be used. It returns the multiplicity by a given object and a relation key. In order to count the users within a group, add the follwing code to your business layer:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // select group $Group = $ORM->loadObjectByID('Group',1); // select amount of users within the group echo $ORM->loadRelationMultiplicity($Group,'Group2User');

4.2.6 Loading the object count

Beneath the amount of objects related to another object, release 1.12 introduces a method to request the amount of objects of a certain kind stored in the database. For this reason, the

  • loadObjectCount()

method has been added. It takes the name of the object as defined in the mappin configuration as it's first parameter. As an optional second parameter, you can pass a GenericCriterionObject that limits the amount of objects using attribute indicators.

Requesting the amount of objects of type User and all users who's last name starts with character A can be requested as follows:

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);
Due to the fact, that this query is not cached, it should not be integrated in performance relevant parts of the application.

4.3. Saving objects

Saving objects is quite easy. Regardless you save objects or object trees, the
  • saveObject()
function can be used. The next code box shows how to save a user:
PHP-Code
// create fabric $ormf = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $orm = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // set some attributes $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // save user $orm->saveObject($user);
As of release 1.11, the saved object (in this case $user) can directly be reused after calling saveObject(). The mapper therefor injects the current mapper instance and the id of the object within the database. Details on the feature request can be taken from the forum post Erweiterung GORM (Release 1.11) (German).

4.4. Saving object trees

As already mentioned above, the OR mapper is not only able to save flat object structures but also object trees. This feature can especially be used within your application's data layer to create the necessary relations.

Problem: When you create a User, it should be composed under the Application object. The latter one is used to ensure multi-client capability for the user management module.

Solution: To create the relation between these two objects (Application and User), the addRelatedObject() method of the GenericDomainObject can be used. The following code box shows, how the implementation is like:
PHP-Code
// create fabric $ormf = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $orm = &$ormf->getGenericORMapper('modules::usermanagement','umgt','umgt'); // load application object $app = $orm->loadObjectByID('Application',1); // fill user $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // compose user $app->addRelatedObject('Application2User',$user); // save object tree $orm->saveObject($app);

Creating relations of type composition must be done exactly as described within the code block. This is because a composed object cannot live without it's father object. Associations can be added afterwards using createAssociation().

In case objects have to be created - as the user in the code snippet above - it is not necessary to save the user before creating it's composition with the application object. The GORM does this implicitly during saving the object tree (consisting of the Application, the User, and the relation between the two objects composing the user under the app).

If you intend to add a group and a role at the same time, the php code above must be added the following lines:
PHP-Code
// load application $app = $orm->loadObjectByID('Application',1); // fill user $user = new GenericDomainObject('User'); $user->setProperty('FirstName','Christian'); $user->setProperty('LastName','Achatz'); // load group $group = $orm->loadObjectByID('Group',1); // load role $role = $orm->loadObjectByID('Role',1); // associate role and group $user->addRelatedObject('Group2User',$group); $user->addRelatedObject('Role2User',$role); // compose user $app->addRelatedObject('Application2User',$user); // save object tree $orm->saveObject($app);

The generic O/R mapper is able to save object trees with various size. Please note, that it may be necessary to treat huge object trees a little bit different. But this is only necessary for applications with high performance requirements. In all other cases, the performance of the GORM is absolutely sufficient.

In case the amount of objects invoked during object tree persistance is greater than 20 including at least one relation per object, it is recommended to save the objects first by using saveObject(). Afterwards, the relations necessary can be created using createAssociation(). This kind of hanling is only possible for associations and cannot be applied for compositions. This is because of the quality of composition relations.

5. The GenericCriterionObject

The present chapter is intended to describe the usage of the GenericCriterionObject. As already mentioned in the chapters above, the class can be used to configure your request rather than to write SQL statements. The object can be used with all load*ByCriterion() methods and while loading relation objects on existing domain objects or domain object lists.

The code box below presents an application use case of the GenericCriterionObject, where a list of users should be loaded. The users should be assigned to a special group and must be composed under a certain application object:

PHP-Code
class UsermanagementManager extends APFObject { public function getUserList(){ // create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // create the criterion object $Crit = new GenericCriterionObject(); // add the relation definition for the composition to the "Application" object $Application = new GenericDomainObject('Application'); $Application->setProperty('ApplicationID',1); $Crit->addRelationIndicator('Application2User',$Application); // add the relation definition for the association to the "Group" object $Group = new GenericDomainObject('Group'); $Group->setProperty('GroupID',1); $Crit->addRelationIndicator('Group2User',$Group); // add a limit indicator $Crit->addCountIndicator(0,3); // add a property indicator applied to the objects to be loaded $Crit->addPropertyIndicator('LastName','Achatz'); // add an order indicator $Crit->addOrderIndicator('FirstName','ASC'); $Crit->addOrderIndicator('LastName','DESC'); // add a directive, which properties should be loaded $Crit->addLoadedProperty('FirstName'); $Crit->addLoadedProperty('LastName'); // load an object list with aid of the criterion object or ... return $ORM->loadObjectListByCriterion('User',$Crit); // ... load an object with aid of the criterion object return $ORM->loadObjectByCriterion('User',$Crit); } }
Notes on the source code:
  • Relations:
    Adding a relation indicator to the criterion object says, that the object or object list to be loaded must have a relation to the object within the criterion. If a relation to the Application object (composition) and to the Group object (association) is added, this means that the resulting object list belongs to a dedicated application and is assigned to a special group.
    If a developer is likely to select all users, that belong to a dedicated application and are are member of a group and are assigned a role, three relation indicators must be added to the criterion object.
  • Sort sequence:
    The order of calls decides the sort sequences. If you like to sort in another direction, the sort indicators must be changed. ASC stands for ascending and DESC for descending sortation.

6. Enhancement of mapping and relation table

If the GenericORRelationMapper should be used for several applications or multiple application cases, sometimes it is good to split the configuration into several pieces. This makes configuration more generic and the mapper can thus be enabled to use specific parts of a greater database. To do so, you have two different choices: you create one configuration set for each application case or you create only a basic configuration, that is interessting for all application cases (e.g. user management) and use the
  • addMappingConfiguration()
  • addRelationConfiguration()
to extend the configuration for the special use cas. With these functions, any object and relation configuration files can be added. The following example shows to you, how the functions can be used to upgrade the scope of the mapper:
PHP-Code
// create fabric $ORMF = &$this->__getServiceObject('modules::genericormapper::data','GenericORMapperFactory'); // create mapper $ORM = &$ORMF->getGenericORMapper('modules::usermanagement','umgt','umgt'); // add another object definition $oRM->addMappingConfiguration('modules::usermanagement','umgt_2'); // add another relation definition $oRM->addRelationConfiguration('modules::usermanagement','umgt_2');
The syntax of the object and relation configuration is the same as the default configuration described in the chapter 2.3. Object and relation definition. The additional object configuration file contains the following objects:
APF-Konfiguration
[Project] DisplayName = "VARCHAR(100)" Description = "TEXT" [News] DisplayName = "VARCHAR(100)" Title = "VARCHAR(100)" Content = "TEXT"
and the newly added relations are
APF-Konfiguration
[Application2Project] Type = "COMPOSITION" SourceObject = "Application" TargetObject = "Project" [Project2News] Type = "COMPOSITION" SourceObject = "Project" TargetObject = "News"

7. Notes

Hints on performance relevant issues can be found in the performance hacks chapter, the manual database setup is discussed in the manual database setup section. The source files of the usermanagement module can be used as an extended example.

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.