1. Overview
      1. What It Is
      2. Why I Would Want to Use It
      3. Known Issues
    2. Getting Started
      1. Fundamentals
        1. Design Patterns
        2. xPDO, the Class
          1. As Object and Relational Mapper
          2. As PDO Wrapper
          3. As Service Layer
          4. The xPDO Constructor
            1. Hydrating Fields
      2. Introduction to the xPDO Object Model
        1. Understanding xPDOCriteria and xPDOQuery
      3. Creating a Model With xPDO
        1. Defining a Schema
          1. Defining Relationships
          2. Defining the Database and Tables
            1. Upgrading Models to Schema Version 1.1
          3. More Examples of xPDO XML Schema Files
          4. Validation Rules in your Schema
        2. Generating the Model Code
          1. Domain Classes
          2. O-R Maps
          3. Table Classes
      4. Using Your xPDO Model
        1. Creating Objects
        2. Database Connections and xPDO
        3. Loading Packages
        4. Removing Objects
          1. removeCollection
        5. Retrieving Objects
          1. getCollectionGraph
        6. Setting Object Fields
        7. Working with Related Objects
    3. Extending Your xPDO Model
      1. Adding Business Logic to this CRUD
      2. Overriding Derived Behavior
        1. Using Custom Object Loaders
      3. Working With Transient Object Classes
    4. Advanced Features
      1. Caching
        1. Caching Tutorial - Basic Snippets
        2. Caching Tutorial - Lifetimes
      2. Object Validation
        1. xPDOForeignKeyConstraint
        2. xPDOMaxLengthValidationRule
        3. xPDOMaxValueValidationRule
        4. xPDOMinLengthValidationRule
        5. xPDOMinValueValidationRule
        6. xPDOObjectExistsValidationRule
        7. A New Page
    5. xPDO Development
      1. Testing
      2. Building
      3. Documentation and Tutorials
    6. Class Reference
      1. xPDO
        1. xPDO.addPackage
        2. xPDO.beginTransaction
        3. xPDO.commit
        4. xPDO.connect
        5. xPDO.fromJSON
        6. xPDO.getCollection
        7. xPDO.getCollectionGraph
        8. xPDO.getCount
        9. xPDO.getDebug
        10. xPDO.getFields
        11. xPDO.getIterator
        12. xPDO.getManager
        13. xPDO.getObject
        14. xPDO.getObjectGraph
        15. xPDO.getOption
        16. xPDO.getTableName
        17. xPDO.loadClass
        18. xPDO.log
        19. xPDO.newObject
        20. xPDO.newQuery
        21. xPDO.query
        22. xPDO.setDebug
        23. xPDO.setLogLevel
        24. xPDO.setLogTarget
        25. xPDO.setOption
        26. xPDO.toJSON
      2. xPDOCacheManager
        1. xPDOCacheManager.copyFile
        2. xPDOCacheManager.copyTree
        3. xPDOCacheManager.delete
        4. xPDOCacheManager.deleteTree
        5. xPDOCacheManager.endsWith
        6. xPDOCacheManager.escapeSingleQuotes
        7. xPDOCacheManager.get
        8. xPDOCacheManager.getCachePath
        9. xPDOCacheManager.getCacheProvider
        10. xPDOCacheManager.matches
        11. xPDOCacheManager.replace
        12. xPDOCacheManager.set
        13. xPDOCacheManager.writeFile
        14. xPDOCacheManager.writeTree
      3. xPDOGenerator
        1. xPDOGenerator.parseSchema
        2. xPDOGenerator.writeSchema
      4. xPDOManager
        1. xPDOManager.createObjectContainer
        2. xPDOManager.createSourceContainer
        3. xPDOManager.removeObjectContainer
        4. xPDOManager.removeSourceContainer
      5. xPDOObject
        1. Configuration Accessors
          1. getOption
          2. setOption
        2. Field Accessors
          1. fromArray
          2. fromJSON
          3. get
          4. set
          5. toArray
          6. toJSON
        3. Metadata Accessors
          1. getFieldName
          2. getFKClass
          3. getFKDefinition
          4. getPK
          5. getPKType
          6. getSelectColumns
        4. Persistence Methods
          1. remove
          2. save
        5. Related Object Accessors
          1. addMany
          2. addOne
          3. getMany
          4. getOne
        6. State Accessors
          1. isDirty
          2. isLazy
          3. isNew
        7. Static Object Loaders
          1. _loadCollectionInstance
          2. _loadInstance
          3. _loadRows
          4. load
          5. loadCollection
          6. loadCollectionGraph
          7. Using Custom Loader Classes
        8. Validation
          1. addValidationRule
          2. getValidator
          3. isValidated
          4. removeValidationRules
          5. validate
      6. xPDOQuery
        1. xPDOQuery.andCondition
        2. xPDOQuery.groupby
        3. xPDOQuery.innerJoin
        4. xPDOQuery.leftJoin
        5. xPDOQuery.limit
        6. xPDOQuery.orCondition
        7. xPDOQuery.rightJoin
        8. xPDOQuery.select
        9. xPDOQuery.setClassAlias
        10. xPDOQuery.sortby
        11. xPDOQuery.where
      7. xPDORevisionControl
      8. xPDOTransport
        1. xPDOTransport.install
        2. xPDOTransport.pack
        3. xPDOTransport.uninstall
      9. xPDOValidator
        1. xPDOValidator.addMessage
        2. xPDOValidator.getMessages
        3. xPDOValidator.hasMessages
        4. xPDOValidator.validate

Defining Relationships

Last edited by Susan Ottwell on Sep 15, 2013.

Welcome to the MODX Documentation. It is an ongoing effort of the MODX community. If you would like to participate or if you notice any errors or missing content, please let us know.

We're going to need to define some relationships between our tables so xPDO can communicate properly between them. xPDO deals with two types of relationships, aggregate and composite.

Aggregate Relationships

An aggregate relationship in xPDO is relationship between two tables where the secondary table is an aggregate of the primary table in such a way that if the primary table is deleted, the secondary table should still exist.

A great example of this is a collection of Crayons in a Box. The relationship from the Crayons to the Box is an aggregate relationship, and would be defined in our XML schema like this:

<object class="myCrayon" table="crayons" extends="xPDOSimpleObject">
    <field key="box" dbtype="int" precision="10" phptype="integer" null="false" default="" />
    <aggregate alias="Box" class="myBox" local="box" foreign="id" cardinality="one" owner="foreign" />
</object>

Note the attributes:

  • alias - Relationships in xPDO allow for 'aliases', which can differentiate between two different relationships that refer to the same foreign key.
  • class - The class name of the relating object.
  • local - The local key by which we get the ID of the foreign, related object. In our example, this tells us the ID of the Box.
  • foreign - The foreign key by which this object relates. In our example, it is the ID field of the Box object.
  • cardinality - The cardinality of the relationship. Usually, in aggregates, this is "one", since there is only one Box by which this Crayon could be referring to. In the opposite relationship between the Box to the Crayon (a Composite relationship), the Box could be pointing to many Crayons - so then the value would be "many", not "one". Note that the value you give to cardinality also makes the difference as to if you need to use addOne or addMany when relating objects.
  • owner - The owner of the foreign key that relates the objects. This is "foreign" when the other class you are relating is related to by its primary key (ie you specified foreign="id" in your alias) or "local" if the class you are defining the relationship in is related by its primary key (ie you specified local="id" in your alias). When relating multiple objects (tables) you will always have owner="foreign" in one alias, and owner="local" in the opposite relationship.

So our XML here would allow us to use the following code to grab the Box for a Crayon:

$crayon = $xpdo->getObject('myCrayon',1);
$box = $crayon->getOne('Box');
echo $box->get('name');

Composite Relationships

A composite relationship in xPDO is relationship between two tables where the secondary table(s) are composites of the primary table in such a way that if the primary table is deleted, the secondary table(s) should be removed.

Back to our Crayon-Box example: The Crayons are Composites of the Box object. We'd define that in our XML schema as:

<object class="myBox" table="boxes" extends="xPDOSimpleObject">
    <composite alias="Crayons" class="myCrayon" local="id" foreign="box" cardinality="many" owner="local" />
</object>

As you can see, a few attributes have changed. The alias now is plural, since we could have any number of Crayons related to this Box. Also, the local attribute now points to the ID of this Box; the foreign attribute points to the foreign key 'box' in the Crayon object; the cardinality is now "many"; and finally, the owner of the key is now "local", since it is owned by the Box.

We can grab all the Crayons in the Box with this xPDO code:

<object class="myBox" table="boxes" extends="xPDOSimpleObject">
    <composite alias="Crayons" class="myCrayon" local="id" foreign="box" cardinality="many" owner="local" />
</object>

As you can see, a few attributes have changed. The alias now is plural, since we could have any number of Crayons related to this Box. Also, the local attribute now points to the ID of this Box; the foreign attribute points to the foreign key 'box' in the Crayon object; the cardinality is now "many"; and finally, the owner of the key is now "local", since it is owned by the Box.

We can grab all the Crayons in the Box with this xPDO code:

$box = $xpdo->getObject('myBox',23);
$crayons = $box->getMany('Crayons');
foreach ($crayons as $crayon) {
   echo $crayon->get('color').'<br />';
}

Remember that in a Composite relationship, should the owner of the relationship be removed, all the Composites will be removed. So, if we remove the Box object:

$box->remove();

...this would remove all of the related Crayons for that Box. This can be useful to cascade removal of objects, making code simpler and easier to manage.

Relating Many-to-Many

Let's go back to our StoreFinder model. First off, let's review our schema so far:

<?xml version="1.0" encoding="UTF-8"?>
<model package="storefinder" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" phpdoc-package="storefinder" phpdoc-subpackage="model" version="1.1">
  <object class="sfStore" table="sfinder_stores" extends="xPDOSimpleObject">
    <field key="name" dbtype="varchar" precision="100" phptype="string" null="false" default="" index="index" />
    <field key="address" dbtype="varchar" precision="255" phptype="string" null="false" default="" />
    <field key="city" dbtype="varchar" precision="255" phptype="string" null="false" default="" />
    <field key="state" dbtype="varchar" precision="255" phptype="string" null="false" default="" />
    <field key="zip" dbtype="varchar" precision="10" phptype="string" null="false" default="0" index="index" />
    <field key="country" dbtype="varchar" precision="20" phptype="string" null="false" default="" />
    <field key="phone" dbtype="varchar" precision="20" phptype="string" null="false" default="" />
    <field key="fax" dbtype="varchar" precision="20" phptype="string" null="false" default="" />
    <field key="active" dbtype="int" precision="1" attributes="unsigned" phptype="integer" null="false" default="0" />
    <alias key="postalcode" field="zip" />
    <index alias="name" name="name" primary="false" unique="false" type="BTREE">
        <column key="name" length="" collation="A" null="false" />
    </index>
    <index alias="zip" name="zip" primary="false" unique="false" type="BTREE">
        <column key="zip" length="" collation="A" null="false" />
    </index>
  </object>
  <object class="sfOwner" table="sfinder_owners" extends="xPDOSimpleObject">
    <field key="name" dbtype="varchar" precision="100" phptype="string" null="false" default="" index="index" />
    <field key="email" dbtype="varchar" precision="255" phptype="string" null="false" default="" />
    <index alias="name" name="name" primary="false" unique="false" type="BTREE">
        <column key="name" length="" collation="A" null="false" />
    </index>
  </object>
  <object class="sfStoreOwner" table="sfinder_store_owners" extends="xPDOSimpleObject">
    <field key="store" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" index="index" />
    <field key="owner" dbtype="int" precision="10" attributes="unsigned" phptype="integer" null="false" default="0" index="index" />
    <index alias="store" name="store" primary="false" unique="false" type="BTREE">
        <column key="store" length="" collation="A" null="false" />
    </index>
    <index alias="owner" name="owner" primary="false" unique="false" type="BTREE">
        <column key="owner" length="" collation="A" null="false" />
    </index>
  </object>
</model>

We're going to want to relate Stores to Owners, but as you can see here, the relationship is "many-to-many" - an Owner can have multiple Stores, and a Store can have multiple Owners. So how do we handle this? Well, the best way is to create an intermediary table, which we'll call 'sfStoreOwner'. This table has only 3 fields - its ID, and 2 indexed fields that are 'store' and 'owner'.

Those two fields contain the PK values of the Store and Owner it is relating. So let's add the relationships. In our sfStore definition, we want to add this line:

<composite alias="StoreOwners" class="sfStoreOwner" local="id" foreign="store" cardinality="many" owner="local" />

And in our sfOwner definition, let's add this:

<composite alias="StoreOwners" class="sfStoreOwner" local="id" foreign="owner" cardinality="many" owner="local" />

Note that both of our primary classes use a Composite relationship. This is because if any of our Stores or Owners get deleted, we want to delete any connecting relationships between them.

So go to our sfStoreOwner definition, and add these two lines:

<aggregate alias="Store" class="sfStore" local="store" foreign="id" cardinality="one" owner="foreign" />
<aggregate alias="Owner" class="sfOwner" local="owner" foreign="id" cardinality="one" owner="foreign" />

Now that we've got our model defined, in our xPDO code we'll be able to do something like this:

<aggregate alias="Store" class="sfStore" local="store" foreign="id" cardinality="one" owner="foreign" />
<aggregate alias="Owner" class="sfOwner" local="owner" foreign="id" cardinality="one" owner="foreign" />

Now that we've got our model defined, in our xPDO code we'll be able to do something like this:

$store = $xpdo->getObject('sfStore',43);
$storeOwners = $store->getMany('StoreOwners');
$owners = array();
foreach ($storeOwners as $storeOwner) {
    $owners[] = $storeOwner->getOne('Owner');
}
foreach ($owners as $owner) {
   echo $owner->get('name').'<br />';
}

And that will output a list of owners for that store.

However, as you can see, that code isn't very optimized. So we're going to optimize it a bit using $xpdo->newQuery:

$c = $xpdo->newQuery('sfOwner');
$c->innerJoin('sfStoreOwner','StoreOwners');
$c->where(array(
   'StoreOwners.store' => 43, // the ID of our Store
));
$owners = $xpdo->getCollection('sfOwner',$c);
foreach ($owners as $owner) {
   echo $owner->get('name').'<br />';
}

This block of code lets us grab all the owners of a store with only one query.

Conclusion

Building relationships within schemas obeys some simple rules, you just have to get familiar with which directions the relationships apply. If you require more examples of how to represent your database tables in the xPDO schema, have a look at More Examples of xPDO XML Schema Files.

Now that we've built our schema, let's go ahead and generate the PHP classes and maps.

See Also