From 8708f7e88e423b44ea4b3b8fff14f055d2b5c4ac Mon Sep 17 00:00:00 2001 From: wei <> Date: Fri, 4 May 2007 08:37:12 +0000 Subject: Add saving/updating ActiveRecord relationships. --- .../Data/ActiveRecord/Exceptions/messages.txt | 3 +- .../Relations/TActiveRecordBelongsTo.php | 24 ++++ .../Relations/TActiveRecordHasMany.php | 27 +++++ .../Relations/TActiveRecordHasManyAssociation.php | 132 +++++++++++++++++++-- .../ActiveRecord/Relations/TActiveRecordHasOne.php | 19 +++ .../Relations/TActiveRecordRelation.php | 8 +- .../Relations/TActiveRecordRelationContext.php | 59 +++++++-- framework/Data/ActiveRecord/TActiveRecord.php | 5 +- .../Data/ActiveRecord/TActiveRecordGateway.php | 13 +- .../ActiveRecord/TActiveRecordStateRegistry.php | 27 +++-- 10 files changed, 286 insertions(+), 31 deletions(-) (limited to 'framework/Data/ActiveRecord') diff --git a/framework/Data/ActiveRecord/Exceptions/messages.txt b/framework/Data/ActiveRecord/Exceptions/messages.txt index 6c13450d..fabfc1a4 100644 --- a/framework/Data/ActiveRecord/Exceptions/messages.txt +++ b/framework/Data/ActiveRecord/Exceptions/messages.txt @@ -20,4 +20,5 @@ ar_invalid_finder_class_name = Class name for finder($className) method must ar_invalid_criteria = Invalid criteria object, must be a string or instance of TSqlCriteria. ar_relations_undefined = Unable to determine Active Record relationships because static array property {0}::${1} is not defined. ar_undefined_relation_prop = Unable to find {1}::${2}['{0}'], Active Record relationship definition for property "{0}" not found in entries of {1}::${2}. -ar_invalid_relationship = Invalid active record relationship. \ No newline at end of file +ar_invalid_relationship = Invalid active record relationship. +ar_relations_missing_fk = Unable to find foreign key relationships in table '{0}' that corresponds to table '{1}'. \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php index 1168bf55..9374af64 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php @@ -104,6 +104,30 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation $source->{$prop} = $collections[$hash][0]; } } + + /** + * Updates the source object first. + * @return boolean true if all update are success (including if no update was required), false otherwise . + */ + public function updateAssociatedRecords() + { + $obj = $this->getContext()->getSourceRecord(); + $fkObject = $obj->{$this->getContext()->getProperty()}; + $registry = $fkObject->getRecordManager()->getObjectStateRegistry(); + if($registry->shouldPersistObject($fkObject)) + { + if($fkObject!==null) + { + $fkObject->save(); + $source = $this->getSourceRecord(); + $fkeys = $this->findForeignKeys($source, $fkObject); + foreach($fkeys as $srcKey => $fKey) + $source->{$srcKey} = $fkObject->{$fKey}; + return true; + } + } + return true; + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php index ab28a455..d70f911b 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php @@ -85,6 +85,33 @@ class TActiveRecordHasMany extends TActiveRecordRelation $fkObjects = $this->findForeignObjects($fields,$indexValues); $this->populateResult($results,$properties,$fkObjects,$fields); } + + /** + * Updates the associated foreign objects. + * @return boolean true if all update are success (including if no update was required), false otherwise . + */ + public function updateAssociatedRecords() + { + $obj = $this->getContext()->getSourceRecord(); + $fkObjects = &$obj->{$this->getContext()->getProperty()}; + $success=true; + if(($total = count($fkObjects))> 0) + { + $source = $this->getSourceRecord(); + $registry = $source->getRecordManager()->getObjectStateRegistry(); + $fkeys = $this->findForeignKeys($fkObjects[0], $source); + for($i=0;$i<$total;$i++) + { + if($registry->shouldPersistObject($fkObjects[$i])) + { + foreach($fkeys as $fKey => $srcKey) + $fkObjects[$i]->{$fKey} = $source->{$srcKey}; + $success = $success && $fkObjects[$i]->save(); + } + } + } + return $success; + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php index 50558a2b..2dee2bcd 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php @@ -87,6 +87,7 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation private $_association; private $_sourceTable; private $_foreignTable; + private $_association_columns=array(); /** * Get the foreign key index values from the results and make calls to the @@ -117,8 +118,14 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation { $gateway = $this->getSourceRecord()->getRecordGateway(); $conn = $this->getSourceRecord()->getDbConnection(); - $table = $this->getContext()->getAssociationTable(); - $this->_association = $gateway->getTableInfo($conn, $table); + //table name may include the fk column name separated with a dot. + $table = explode('.', $this->getContext()->getAssociationTable()); + if(count($table)>1) + { + $columns = preg_replace('/^\((.*)\)/', '\1', $table[1]); + $this->_association_columns = preg_split('/\s*[, ]\*/',$columns); + } + $this->_association = $gateway->getTableInfo($conn, $table[0]); } return $this->_association; } @@ -158,6 +165,13 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord()); } + protected function getForeignCommandBuilder() + { + $obj = $this->getContext()->getForeignRecordFinder(); + return $this->getSourceRecord()->getRecordGateway()->getCommand($obj); + } + + /** * Fetches the foreign objects using TActiveRecord::findAllByIndex() * @param array field names @@ -172,12 +186,12 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys); $srcProps = array_keys($sourceKeys); $collections=array(); - foreach($command->query() as $row) + foreach($this->getCommandBuilder()->onExecuteCommand($command, $command->query()) as $row) { $hash = $this->getObjectHash($row, $srcProps); foreach($srcProps as $column) unset($row[$column]); - $obj = new $type($row); + $obj = $this->createFkObject($type,$row,$foreignKeys); $collections[$hash][] = $obj; $registry->registerClean($obj); } @@ -185,6 +199,24 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $this->setResultCollection($results, $collections, array_values($sourceKeys)); } + /** + * @param string active record class name. + * @param array row data + * @param array foreign key column names + * @return TActiveRecord + */ + protected function createFkObject($type,$row,$foreignKeys) + { + $obj = new $type($row); + if(count($this->_association_columns) > 0) + { + $i=0; + foreach($foreignKeys as $ref=>$fk) + $obj->{$ref} = $row[$this->_association_columns[$i++]]; + } + return $obj; + } + /** * @param TSqlCriteria * @param TTableInfo association table info @@ -205,7 +237,7 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $limit = $criteria->getLimit(); $offset = $criteria->getOffset(); - $builder = $this->getCommandBuilder()->getBuilder(); + $builder = $this->getForeignCommandBuilder()->getBuilder(); $command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset); $this->getCommandBuilder()->onCreateCommand($command, $criteria); return $command; @@ -220,7 +252,8 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $columns=array(); $table = $this->getAssociationTable(); $tableName = $table->getTableFullName(); - foreach($sourceKeys as $name=>$fkName) + $columnNames = array_merge(array_keys($sourceKeys),$this->_association_columns); + foreach($columnNames as $name) $columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName(); return implode(', ', $columns); } @@ -241,9 +274,14 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $fkTable = $fkInfo->getTableFullName(); $joins = array(); + $hasAssociationColumns = count($this->_association_columns) > 0; + $i=0; foreach($foreignKeys as $ref=>$fk) { - $refField = $refInfo->getColumn($ref)->getColumnName(); + if($hasAssociationColumns) + $refField = $refInfo->getColumn($this->_association_columns[$i++])->getColumnName(); + else + $refField = $refInfo->getColumn($ref)->getColumnName(); $fkField = $fkInfo->getColumn($fk)->getColumnName(); $joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}"; } @@ -251,5 +289,85 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo,array_keys($sourceKeys), $indexValues); return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}"; } + + /** + * Updates the associated foreign objects. + * @return boolean true if all update are success (including if no update was required), false otherwise . + */ + public function updateAssociatedRecords() + { + $obj = $this->getContext()->getSourceRecord(); + $fkObjects = &$obj->{$this->getContext()->getProperty()}; + $success=true; + if(($total = count($fkObjects))> 0) + { + $source = $this->getSourceRecord(); + $registry = $source->getRecordManager()->getObjectStateRegistry(); + $builder = $this->getAssociationTableCommandBuilder(); + for($i=0;$i<$total;$i++) + { + if($registry->shouldPersistObject($fkObjects[$i])) + $success = $success && $fkObjects[$i]->save(); + } + return $this->updateAssociationTable($obj, $fkObjects, $builder) && $success; + } + return $success; + } + + protected function getAssociationTableCommandBuilder() + { + $conn = $this->getContext()->getSourceRecord()->getDbConnection(); + return $this->getAssociationTable()->createCommandBuilder($conn); + } + + protected function hasAssociationData($builder,$data) + { + $condition=array(); + $table = $this->getAssociationTable(); + foreach($data as $name=>$value) + $condition[] = $table->getColumn($name)->getColumnName().' = ?'; + $command = $builder->createCountCommand(implode(' AND ', $condition),array_values($data)); + $result = $this->getCommandBuilder()->onExecuteCommand($command, intval($command->queryScalar())); + return intval($result) > 0; + } + + protected function addAssociationData($builder,$data) + { + $command = $builder->createInsertCommand($data); + return $this->getCommandBuilder()->onExecuteCommand($command, $command->execute()) > 0; + } + + protected function updateAssociationTable($obj,$fkObjects, $builder) + { + $source = $this->getSourceRecordValues($obj); + $foreignKeys = $this->findForeignKeys($this->getAssociationTable(), $fkObjects[0]); + $success=true; + foreach($fkObjects as $fkObject) + { + $data = array_merge($source, $this->getForeignObjectValues($foreignKeys,$fkObject)); + if(!$this->hasAssociationData($builder,$data)) + $success = $this->addAssociationData($builder,$data) && $success; + } + return $success; + } + + protected function getSourceRecordValues($obj) + { + $sourceKeys = $this->findForeignKeys($this->getAssociationTable(), $obj); + $indexValues = $this->getIndexValues(array_values($sourceKeys), $obj); + $data = array(); + $i=0; + foreach($sourceKeys as $name=>$srcKey) + $data[$name] = $indexValues[0][$i++]; + return $data; + } + + protected function getForeignObjectValues($foreignKeys,$fkObject) + { + $data=array(); + foreach($foreignKeys as $name=>$fKey) + $data[$name] = $fkObject->{$fKey}; + return $data; + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php index 6348f16b..7127d2ac 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php @@ -119,6 +119,25 @@ class TActiveRecordHasOne extends TActiveRecordRelation $source->{$prop} = $collections[$hash][0]; } } + + /** + * Updates the associated foreign objects. + * @return boolean true if all update are success (including if no update was required), false otherwise . + */ + public function updateAssociatedRecords() + { + $fkObject = $this->getContext()->getPropertyValue(); + $registry = $fkObject->getRecordManager()->getObjectStateRegistry(); + if($registry->shouldPersistObject($fkObject)) + { + $source = $this->getSourceRecord(); + $fkeys = $this->findForeignKeys($fkObject, $source); + foreach($fkeys as $fKey => $srcKey) + $fkObject->{$fKey} = $source->{$srcKey}; + return $fkObject->save(); + } + return true; + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php index 46609095..4dc9743f 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php @@ -95,7 +95,9 @@ abstract class TActiveRecordRelation if($fkeys['table']===$matchingTableName) return $fkeys['keys']; } - throw new TActiveRecordException('no fk defined for '.$tableInfo->getTableFullName()); + $matching = $gateway->getRecordTableInfo($matchesRecord)->getTableFullName(); + throw new TActiveRecordException('ar_relations_missing_fk', + $tableInfo->getTableFullName(), $matching); } /** @@ -189,6 +191,10 @@ abstract class TActiveRecordRelation $prop = $this->getContext()->getProperty(); $source->{$prop} = isset($collections[$hash]) ? $collections[$hash] : array(); } + + public function updateAssociatedRecords() + { + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php index 167c90a5..e601eb53 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -35,38 +35,52 @@ class TActiveRecordRelationContext private $_criteria; private $_relation; - public function __construct($source, $property, $criteria) + public function __construct($source, $property=null, $criteria=null) { $this->_sourceRecord=$source; - $this->_property=$property; $this->_criteria=$criteria; - $this->_relation = $this->getSourceRecordRelation($property); + if($property!==null) + list($this->_property, $this->_relation) = $this->getSourceRecordRelation($property); } /** * Uses ReflectionClass to obtain the relation details array of a given * property from the $RELATIONS static property in TActiveRecord. * @param string relation property name - * @return array relation definition. + * @return array array($propertyName, $relation) relation definition. * @throws TActiveRecordException if property is not defined or missing. */ 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); $property = strtolower($property); - foreach($statics[self::RELATIONS_CONST] as $name => $relation) + foreach($this->getRecordRelationships() as $name => $relation) { if(strtolower($name)===$property) - return $relation; + return array($name, $relation); } throw new TActiveRecordException('ar_undefined_relation_prop', $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); } + /** + * @return array the key and values of TActiveRecord::$RELATIONS + */ + public function getRecordRelationships() + { + $class = new ReflectionClass($this->_sourceRecord); + $statics = $class->getStaticProperties(); + if(isset($statics[self::RELATIONS_CONST])) + return $statics[self::RELATIONS_CONST]; + else + return array(); + } + + public function getPropertyValue() + { + $obj = $this->getSourceRecord(); + return $obj->{$this->getProperty()}; + } + /** * @return string name of the record property that the relationship results will be assigned to. */ @@ -162,6 +176,29 @@ class TActiveRecordRelationContext throw new TActiveRecordException('ar_invalid_relationship'); } } + + /** + * @return TActiveRecordRelationCommand + */ + public function updateAssociatedRecords($updateBelongsTo=false) + { + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationCommand'); + $success=true; + foreach($this->getRecordRelationships() as $property=>$relation) + { + $belongsTo = $relation[0]==TActiveRecord::BELONGS_TO; + if(($updateBelongsTo && $belongsTo) || (!$updateBelongsTo && !$belongsTo)) + { + $obj = $this->getSourceRecord(); + if(!empty($obj->{$property})) + { + $context = new self($this->getSourceRecord(),$property); + $success = $success && $context->getRelationHandler()->updateAssociatedRecords(); + } + } + } + return $success; + } } ?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php index 479f643b..1f482e6a 100644 --- a/framework/Data/ActiveRecord/TActiveRecord.php +++ b/framework/Data/ActiveRecord/TActiveRecord.php @@ -316,11 +316,14 @@ abstract class TActiveRecord extends TComponent //create and populate the object $obj = Prado::createComponent($type); $tableInfo = $this->getRecordGateway()->getRecordTableInfo($obj); + foreach($data as $name=>$value) + $obj->{$name} = $value; + /* foreach($tableInfo->getColumns()->getKeys() as $name) { if(isset($data[$name])) $obj->{$name} = $data[$name]; - } + }*/ $obj->_readOnly = $tableInfo->getIsView(); $this->getRecordManager()->getObjectStateRegistry()->registerClean($obj); return $obj; diff --git a/framework/Data/ActiveRecord/TActiveRecordGateway.php b/framework/Data/ActiveRecord/TActiveRecordGateway.php index 40948999..d97844f7 100644 --- a/framework/Data/ActiveRecord/TActiveRecordGateway.php +++ b/framework/Data/ActiveRecord/TActiveRecordGateway.php @@ -244,9 +244,11 @@ class TActiveRecordGateway extends TComponent */ public function insert(TActiveRecord $record) { + $this->updateAssociatedRecords($record,true); $result = $this->getCommand($record)->insert($this->getInsertValues($record)); if($result) $this->updatePostInsert($record); + $this->updateAssociatedRecords($record); return $result; } @@ -297,8 +299,11 @@ class TActiveRecordGateway extends TComponent */ public function update(TActiveRecord $record) { + $this->updateAssociatedRecords($record,true); list($data, $keys) = $this->getUpdateValues($record); - return $this->getCommand($record)->updateByPk($data, $keys); + $result = $this->getCommand($record)->updateByPk($data, $keys); + $this->updateAssociatedRecords($record); + return $result; } protected function getUpdateValues(TActiveRecord $record) @@ -325,6 +330,12 @@ class TActiveRecordGateway extends TComponent return array($values,$primary); } + protected function updateAssociatedRecords(TActiveRecord $record,$updateBelongsTo=false) + { + $context = new TActiveRecordRelationContext($record); + return $context->updateAssociatedRecords($updateBelongsTo); + } + /** * Delete the record. * @param TActiveRecord record to be deleted. diff --git a/framework/Data/ActiveRecord/TActiveRecordStateRegistry.php b/framework/Data/ActiveRecord/TActiveRecordStateRegistry.php index d6f24961..7a285274 100644 --- a/framework/Data/ActiveRecord/TActiveRecordStateRegistry.php +++ b/framework/Data/ActiveRecord/TActiveRecordStateRegistry.php @@ -97,7 +97,7 @@ class TActiveRecordStateRegistry public function registerClean($obj) { $this->removeCleanOrDirty($obj); - if($this->getIsRemovedObject($obj)) + if($this->isRemovedObject($obj)) throw new TActiveRecordException('ar_object_marked_for_removal'); $this->_cleanObjects[] = array($obj, clone($obj)); } @@ -152,7 +152,7 @@ class TActiveRecordStateRegistry * @param TActiveRecord object to test. * @return boolean true if the object is dirty, false otherwise. */ - public function getIsDirtyObject($obj) + public function isDirtyObject($obj) { foreach($this->_cleanObjects as $cache) if($cache[0] === $obj) @@ -165,7 +165,7 @@ class TActiveRecordStateRegistry * @param TActiveRecord object to test. * @return boolean true if object is clean, false otherwise. */ - public function getIsCleanObject($obj) + public function isCleanObject($obj) { foreach($this->_cleanObjects as $cache) if($cache[0] === $obj) @@ -178,21 +178,30 @@ class TActiveRecordStateRegistry * @param TActiveRecord object to test. * @return boolean true if object is newly created, false otherwise. */ - public function getIsNewObject($obj) + public function isNewObject($obj) { - if($this->getIsRemovedObject($obj)) return false; + if($this->isRemovedObject($obj)) return false; foreach($this->_cleanObjects as $cache) if($cache[0] === $obj) return false; return true; } + /** + * @param TActiveRecord object to test. + * @return boolean true if object is dirty or is new. + */ + public function shouldPersistObject($obj) + { + return $this->isDirtyObject($obj) || $this->isNewObject($obj); + } + /** * Test whether an object is marked for deletion. * @param TActiveRecord object to test. * @return boolean true if object is marked for deletion, false otherwise. */ - public function getIsRemovedObject($obj) + public function isRemovedObject($obj) { return $this->_removedObjects->contains($obj); } @@ -211,7 +220,7 @@ class TActiveRecordStateRegistry { $rowsAffected=false; - if($this->getIsRemovedObject($record)) + if($this->isRemovedObject($record)) { $rowsAffected = $gateway->delete($record); if($rowsAffected) @@ -219,9 +228,9 @@ class TActiveRecordStateRegistry } else { - if($this->getIsDirtyObject($record)) + if($this->isDirtyObject($record)) $rowsAffected = $gateway->update($record); - else if($this->getIsNewObject($record)) + else if($this->isNewObject($record)) $rowsAffected = $gateway->insert($record); if($rowsAffected) -- cgit v1.2.3