From d5eb713888715e8f18d2ccf508a8eb0b1a483ad1 Mon Sep 17 00:00:00 2001 From: wei <> Date: Tue, 24 Apr 2007 06:14:56 +0000 Subject: add active record Relations --- .../Relations/TActiveRecordBelongsTo.php | 43 ++++++ .../Relations/TActiveRecordHasMany.php | 96 ++++++++++++ .../Relations/TActiveRecordHasManyAssociation.php | 149 ++++++++++++++++++ .../ActiveRecord/Relations/TActiveRecordHasOne.php | 42 +++++ .../Relations/TActiveRecordRelation.php | 172 +++++++++++++++++++++ .../Relations/TActiveRecordRelationContext.php | 104 +++++++++++++ 6 files changed, 606 insertions(+) create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php create mode 100644 framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php (limited to 'framework/Data/ActiveRecord/Relations') diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php new file mode 100644 index 00000000..3bb1a74b --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php @@ -0,0 +1,43 @@ +getContext()->getForeignRecordFinder(); + $fkeys = $this->findForeignKeys($this->getSourceRecord(),$fkObject); + + $properties = array_keys($fkeys); + $fields = array_values($fkeys); + + $indexValues = $this->getIndexValues($properties, $results); + $fkObjects = $this->findForeignObjects($fields, $indexValues); + $this->populateResult($results,$properties,$fkObjects,$fields); + } + + /** + * Sets the foreign objects to the given property on the source object. + * @param TActiveRecord source object. + * @param array foreign objects. + */ + protected function setObjectProperty($source, $properties, &$collections) + { + $hash = $this->getObjectHash($source, $properties); + $prop = $this->getContext()->getProperty(); + if(isset($collections[$hash]) && count($collections[$hash]) > 0) + { + if(count($collections[$hash]) > 1) + throw new TActiveRecordException('ar_belongs_to_multiple_result'); + $source->{$prop} = $collections[$hash][0]; + } + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php new file mode 100644 index 00000000..27ffd194 --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php @@ -0,0 +1,96 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2007 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Data.ActiveRecord.Relations + */ + +/** + * Loads base active record relations class. + */ +Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation'); + +/** + * Implements TActiveRecord::HAS_MANY relationship between the source object having zero or + * more foreign objects. Consider the relationship between a Team and a Player. + * + * +------+ +--------+ + * | Team | 1 -----> * | Player | + * +------+ +--------+ + * + * Where one team may have 0 or more players and each player belongs to only + * one team. We may model Team-Player relationship as active record as follows. + * + * class TeamRecord extends TActiveRecord + * { + * const TABLE='team'; + * public $name; //primary key + * public $location; + * + * public $players=array(); //list of players + * + * protected static $RELATIONS=array( + * 'players' => array(self::HAS_MANY, 'PlayerRecord')); + * + * public static function finder($className=__CLASS__) + * { + * return parent::finder($className); + * } + * } + * class PlayerRecord extends TActiveRecord + * { + * const TABLE='player'; + * public $player_id; //primary key + * public $team_name; //foreign key player.team_name <-> team.name + * public $age; + * + * public static function finder($className=__CLASS__) + * { + * return parent::finder($className); + * } + * } + * + * The $RELATIONS static property of TeamRecord defines that the + * property $players has many PlayerRecords. + * + * The players list may be fetched as follows. + * + * $team = TeamRecord::finder()->with_players()->findAll(); + * + * The method with_xxx() (where xxx is the relationship property + * name, in this case, players) fetchs the corresponding PlayerRecords using + * a second query (not by using a join). The with_xxx() accepts the same + * arguments as other finder methods of TActiveRecord, e.g. with_player('age < ?', 35). + * + * @author Wei Zhuo + * @version $Id$ + * @package System.Data.ActiveRecord.Relations + * @since 3.1 + */ +class TActiveRecordHasMany extends TActiveRecordRelation +{ + /** + * Get the foreign key index values from the results and make calls to the + * database to find the corresponding foreign objects. + * @param array original results. + */ + protected function collectForeignObjects(&$results) + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); + + $properties = array_values($fkeys); + $fields = array_keys($fkeys); + + $indexValues = $this->getIndexValues($properties, $results); + $fkObjects = $this->findForeignObjects($fields,$indexValues); + $this->populateResult($results,$properties,$fkObjects,$fields); + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php new file mode 100644 index 00000000..a86fdffd --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php @@ -0,0 +1,149 @@ +getAssociationTable(); + $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord()); + + $properties = array_values($sourceKeys); + + $indexValues = $this->getIndexValues($properties, $results); + + $fkObject = $this->getContext()->getForeignRecordFinder(); + $foreignKeys = $this->findForeignKeys($association, $fkObject); + + $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys); + } + + protected function getAssociationTable() + { + if($this->_association===null) + { + $gateway = $this->getSourceRecord()->getRecordGateway(); + $conn = $this->getSourceRecord()->getDbConnection(); + $table = $this->getContext()->getAssociationTable(); + $this->_association = $gateway->getTableInfo($conn, $table); + } + return $this->_association; + } + + protected function getSourceTable() + { + if($this->_sourceTable===null) + { + $gateway = $this->getSourceRecord()->getRecordGateway(); + $this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord()); + } + return $this->_sourceTable; + } + + protected function getForeignTable() + { + if($this->_foreignTable===null) + { + $gateway = $this->getSourceRecord()->getRecordGateway(); + $fkObject = $this->getContext()->getForeignRecordFinder(); + $this->_foreignTable = $gateway->getRecordTableInfo($fkObject); + } + return $this->_foreignTable; + } + + protected function getCommandBuilder() + { + return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord()); + } + + /** + * Fetches the foreign objects using TActiveRecord::findAllByIndex() + * @param array field names + * @param array foreign key index values. + */ + protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys) + { + $criteria = $this->getContext()->getCriteria(); + $finder = $this->getContext()->getForeignRecordFinder(); + $command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys); + $srcProps = array_keys($sourceKeys); + $type = get_class($finder); + $collections=array(); + foreach($command->query() as $row) + { + $hash = $this->getObjectHash($row, $srcProps); + foreach($srcProps as $column) + unset($row[$column]); + $collections[$hash][] = $finder->populateObject($type,$row); + } + + $this->setResultCollection($results, $collections, array_values($sourceKeys)); + } + + /** + * @param TSqlCriteria + * @param TTableInfo association table info + * @param array field names + * @param array field values + */ + public function createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys) + { + $innerJoin = $this->getAssociationJoin($foreignKeys,$indexValues,$sourceKeys); + $fkTable = $this->getForeignTable()->getTableFullName(); + $srcColumns = $this->getSourceColumns($sourceKeys); + if(($where=$criteria->getCondition())===null) + $where='1=1'; + $sql = "SELECT {$fkTable}.*, {$srcColumns} FROM {$fkTable} {$innerJoin} WHERE {$where}"; + + $parameters = $criteria->getParameters()->toArray(); + $ordering = $criteria->getOrdersBy(); + $limit = $criteria->getLimit(); + $offset = $criteria->getOffset(); + + $builder = $this->getCommandBuilder()->getBuilder(); + $command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset); + $this->getCommandBuilder()->onCreateCommand($command, $criteria); + return $command; + } + + protected function getSourceColumns($sourceKeys) + { + $columns=array(); + $table = $this->getAssociationTable(); + $tableName = $table->getTableFullName(); + foreach($sourceKeys as $name=>$fkName) + $columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName(); + return implode(', ', $columns); + } + + protected function getAssociationJoin($foreignKeys,$indexValues,$sourceKeys) + { + $refInfo= $this->getAssociationTable(); + $fkInfo = $this->getForeignTable(); + + $refTable = $refInfo->getTableFullName(); + $fkTable = $fkInfo->getTableFullName(); + + $joins = array(); + foreach($foreignKeys as $ref=>$fk) + { + $refField = $refInfo->getColumn($ref)->getColumnName(); + $fkField = $fkInfo->getColumn($fk)->getColumnName(); + $joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}"; + } + $joinCondition = implode(' AND ', $joins); + + $index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo,array_keys($sourceKeys), $indexValues); + + return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}"; + } +} +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php new file mode 100644 index 00000000..048f776b --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php @@ -0,0 +1,42 @@ +getContext()->getForeignRecordFinder(); + $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); + + $properties = array_values($fkeys); + $fields = array_keys($fkeys); + + $indexValues = $this->getIndexValues($properties, $results); + $fkObjects = $this->findForeignObjects($fields,$indexValues); + $this->populateResult($results,$properties,$fkObjects,$fields); + } + + /** + * Sets the foreign objects to the given property on the source object. + * @param TActiveRecord source object. + * @param array foreign objects. + */ + protected function setObjectProperty($source, $properties, &$collections) + { + $hash = $this->getObjectHash($source, $properties); + $prop = $this->getContext()->getProperty(); + if(isset($collections[$hash]) && count($collections[$hash]) > 0) + { + if(count($collections[$hash]) > 1) + throw new TActiveRecordException('ar_belongs_to_multiple_result'); + $source->{$prop} = $collections[$hash][0]; + } + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php new file mode 100644 index 00000000..f1e8fa60 --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php @@ -0,0 +1,172 @@ +_context = $context; + } + + /** + * @return TActiveRecordRelationContext + */ + protected function getContext() + { + return $this->_context; + } + + /** + * @return TActiveRecord + */ + protected function getSourceRecord() + { + return $this->getContext()->getSourceRecord(); + } + + /** + * Dispatch the method calls to the source record finder object. When + * the results are returned as array or is an instance of TActiveRecord we + * will fetch the corresponding foreign objects with an sql query and populate + * the results obtained earlier. + * + * Allows chaining multiple relation handlers. + * + * @param string method name called + * @param array method arguments + * @return mixed TActiveRecord or array of TActiveRecord results depending on the method called. + */ + public function __call($method,$args) + { + static $stack=array(); + + $results = call_user_func_array(array($this->getSourceRecord(),$method),$args); + if(is_array($results) || $results instanceof TActiveRecord) + { + $this->collectForeignObjects($results); + while($obj = array_pop($stack)) + $obj->collectForeignObjects($results); + } + else if($results instanceof TActiveRecordRelation) + array_push($stack,$this); //call it later + return $results; + } + + /** + * Returns foreign keys in $fromRecord with source column names as key + * and foreign column names in the corresponding $matchesRecord as value. + * The method returns the first matching foreign key between these 2 records. + * @param TActiveRecord $fromRecord + * @param TActiveRecord $matchesRecord + * @return array foreign keys with source column names as key and foreign column names as value. + */ + protected function findForeignKeys($from, $matchesRecord) + { + $gateway = $matchesRecord->getRecordGateway(); + $matchingTableName = $gateway->getRecordTableInfo($matchesRecord)->getTableName(); + $tableInfo=$from; + if($from instanceof TActiveRecord) + $tableInfo = $gateway->getRecordTableInfo($from); + foreach($tableInfo->getForeignKeys() as $fkeys) + { + if($fkeys['table']===$matchingTableName) + return $fkeys['keys']; + } + throw new TActiveRecordException('no fk defined for '.$tableInfo->getTableFullName()); + } + + /** + * @param mixed object or array to be hashed + * @param array name of property for hashing the properties. + * @return string object hash using crc32 and serialize. + */ + protected function getObjectHash($obj, $properties) + { + $ids=array(); + foreach($properties as $property) + $ids[] = is_object($obj) ? $obj->{$property} : $obj[$property]; + return sprintf('%x',crc32(serialize($ids))); + } + + /** + * Fetches the foreign objects using TActiveRecord::findAllByIndex() + * @param array field names + * @param array foreign key index values. + * @return TActiveRecord[] foreign objects. + */ + protected function findForeignObjects($fields, $indexValues) + { + $criteria = $this->getContext()->getCriteria(); + $finder = $this->getContext()->getForeignRecordFinder(); + return $finder->findAllByIndex($criteria, $fields, $indexValues); + } + + /** + * Obtain the foreign key index values from the results. + * @param array property names + * @param array|TActiveRecord TActiveRecord results + * @return array foreign key index values. + */ + protected function getIndexValues($keys, $results) + { + if(!is_array($results)) + $results = array($results); + foreach($results as $result) + { + $value = array(); + foreach($keys as $name) + $value[] = $result->{$name}; + $values[] = $value; + } + return $values; + } + + /** + * Populate the results with the foreign objects found. + * @param array source results + * @param array source property names + * @param array foreign objects + * @param array foreign object field names. + */ + protected function populateResult(&$results,$properties,&$fkObjects,$fields) + { + $collections=array(); + foreach($fkObjects as $fkObject) + { + $hash = $this->getObjectHash($fkObject, $fields); + $collections[$hash][]=$fkObject; + } + + $this->setResultCollection($results, $collections, $properties); + } + + protected function setResultCollection(&$results, &$collections, $properties) + { + if(is_array($results)) + { + for($i=0,$k=count($results);$i<$k;$i++) + $this->setObjectProperty($results[$i], $properties, $collections); + } + else + { + $this->setObjectProperty($results, $properties, $collections); + } + } + + /** + * Sets the foreign objects to the given property on the source object. + * @param TActiveRecord source object. + * @param array source properties + * @param array foreign objects. + */ + protected function setObjectProperty($source, $properties, &$collections) + { + $hash = $this->getObjectHash($source, $properties); + $prop = $this->getContext()->getProperty(); + $source->{$prop} = isset($collections[$hash]) ? $collections[$hash] : array(); + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php new file mode 100644 index 00000000..03dc4cd5 --- /dev/null +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -0,0 +1,104 @@ +_sourceRecord=$source; + $this->_property=$property; + $this->_criteria=$criteria; + $this->_relation = $this->getSourceRecordRelation($property); + } + /** + * @param string relation property name + * @return array relation definition. + */ + protected function getSourceRecordRelation($property) + { + $class = new ReflectionClass($this->_sourceRecord); + $statics = $class->getStaticProperties(); + if(!isset($statics[self::RELATIONS_CONST])) + throw new TActiveRecordException('ar_relations_undefined', + get_class($this->_sourceRecord), self::RELATIONS_CONST); + if(isset($statics[self::RELATIONS_CONST][$property])) + return $statics[self::RELATIONS_CONST][$property]; + else + throw new TActiveRecordException('ar_undefined_relation_prop', + $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); + } + + public function getProperty() + { + return $this->_property; + } + + public function getCriteria() + { + return $this->_criteria; + } + + public function getSourceRecord() + { + return $this->_sourceRecord; + } + + public function getForeignRecordClass() + { + return $this->_relation[1]; + } + + public function getRelationType() + { + return $this->_relation[0]; + } + + public function getAssociationTable() + { + return $this->_relation[2]; + } + + public function hasAssociationTable() + { + return isset($this->_relation[2]); + } + + public function getForeignRecordFinder() + { + return TActiveRecord::finder($this->getForeignRecordClass()); + } + + public function getRelationHandler() + { + switch($this->getRelationType()) + { + case TActiveRecord::HAS_MANY: + if(!$this->hasAssociationTable()) + { + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); + return new TActiveRecordHasMany($this); + } + else + { + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); + return new TActiveRecordHasManyAssociation($this); + } + case TActiveRecord::HAS_ONE: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne'); + return new TActiveRecordHasOne($this); + case TActiveRecord::BELONGS_TO: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo'); + return new TActiveRecordBelongsTo($this); + default: + throw new TException('Not done yet'); + } + } +} + +?> \ No newline at end of file -- cgit v1.2.3