From 4ceba82b9863f2c6323cbe00407e4bfbedbfc1cd Mon Sep 17 00:00:00 2001 From: wei <> Date: Mon, 8 Oct 2007 03:24:07 +0000 Subject: Allow active records to have multiple foreign key references to the same table. Add TXCache. --- .gitattributes | 3 + UPGRADE | 7 + demos/northwind-db/protected/database/Employee.php | 2 +- .../protected/pages/Database/ActiveRecord.page | 52 ++++-- .../protected/pages/Database/ar_objects.png | Bin 19837 -> 20638 bytes .../protected/pages/Database/ar_objects.vsd | Bin 190976 -> 190976 bytes .../protected/pages/Database/id/ActiveRecord.page | 8 +- framework/Caching/TXCache.php | 126 +++++++++++++++ .../Relations/TActiveRecordBelongsTo.php | 13 +- .../Relations/TActiveRecordHasMany.php | 17 +- .../Relations/TActiveRecordHasManyAssociation.php | 24 +-- .../ActiveRecord/Relations/TActiveRecordHasOne.php | 14 +- .../Relations/TActiveRecordRelation.php | 60 ++++++- .../Relations/TActiveRecordRelationContext.php | 91 +++++++---- framework/Data/ActiveRecord/TActiveRecord.php | 75 ++++++++- .../ActiveRecord/ForeignKeyTestCase.php | 9 +- .../ActiveRecord/ForeignObjectUpdateTest.php | 5 +- .../ActiveRecord/MultipleForeignKeyTestCase.php | 179 +++++++++++++++++++++ .../ActiveRecord/records/ItemRecord.php | 2 +- tests/simple_unit/ActiveRecord/test1.sqlite | Bin 0 -> 6144 bytes tests/test_tools/simpletest/test_case.php | 3 +- 21 files changed, 607 insertions(+), 83 deletions(-) create mode 100644 framework/Caching/TXCache.php create mode 100644 tests/simple_unit/ActiveRecord/MultipleForeignKeyTestCase.php create mode 100644 tests/simple_unit/ActiveRecord/test1.sqlite diff --git a/.gitattributes b/.gitattributes index 298a0308..a37df6f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1969,6 +1969,7 @@ framework/Caching/TCache.php -text framework/Caching/TDbCache.php -text framework/Caching/TMemCache.php -text framework/Caching/TSqliteCache.php -text +framework/Caching/TXCache.php -text framework/Collections/TAttributeCollection.php -text framework/Collections/TDummyDataSource.php -text framework/Collections/TList.php -text @@ -3077,6 +3078,7 @@ tests/simple_unit/ActiveRecord/FindByPksTestCase.php -text tests/simple_unit/ActiveRecord/FindBySqlTestCase.php -text tests/simple_unit/ActiveRecord/ForeignKeyTestCase.php -text tests/simple_unit/ActiveRecord/ForeignObjectUpdateTest.php -text +tests/simple_unit/ActiveRecord/MultipleForeignKeyTestCase.php -text tests/simple_unit/ActiveRecord/RecordEventTestCase.php -text tests/simple_unit/ActiveRecord/SqliteTestCase.php -text tests/simple_unit/ActiveRecord/UserRecordTestCase.php -text @@ -3093,6 +3095,7 @@ tests/simple_unit/ActiveRecord/records/SimpleUser.php -text tests/simple_unit/ActiveRecord/records/SqliteUsers.php -text tests/simple_unit/ActiveRecord/records/UserRecord.php -text tests/simple_unit/ActiveRecord/sqlite.sql -text +tests/simple_unit/ActiveRecord/test1.sqlite -text tests/simple_unit/DbCommon/CommandBuilderMssqlTest.php -text tests/simple_unit/DbCommon/CommandBuilderMysqlTest.php -text tests/simple_unit/DbCommon/CommandBuilderPgsqlTest.php -text diff --git a/UPGRADE b/UPGRADE index 17ae3298..334f8231 100644 --- a/UPGRADE +++ b/UPGRADE @@ -11,6 +11,13 @@ for both A and B. Upgrading from v3.1.1 --------------------- +- The RELATIONS type declaration in Active Record classes for Many-to-Many using + an association table was change from "self::HAS_MANY" to "self::MANY_TO_MANY". + E.g. change + 'albums' => array(self::HAS_MANY, 'Artist', 'album_artists') + to + 'albums' => array(self::MANY_TO_MANY, 'Artist', 'album_artists') + Upgrading from v3.1.0 --------------------- diff --git a/demos/northwind-db/protected/database/Employee.php b/demos/northwind-db/protected/database/Employee.php index 92de3f24..1e0f090e 100644 --- a/demos/northwind-db/protected/database/Employee.php +++ b/demos/northwind-db/protected/database/Employee.php @@ -32,7 +32,7 @@ class Employee extends TActiveRecord public static $RELATIONS = array ( - 'Territories' => array(self::HAS_MANY, 'Territory', 'EmployeeTerritories'), + 'Territories' => array(self::MANY_TO_MANY, 'Territory', 'EmployeeTerritories'), 'Orders' => array(self::HAS_MANY, 'Order'), //parent children relationship diff --git a/demos/quickstart/protected/pages/Database/ActiveRecord.page b/demos/quickstart/protected/pages/Database/ActiveRecord.page index a6e087b9..cb10d184 100644 --- a/demos/quickstart/protected/pages/Database/ActiveRecord.page +++ b/demos/quickstart/protected/pages/Database/ActiveRecord.page @@ -657,10 +657,30 @@ in Active Record by inspecting the Players and Teams table def

Info: -Active Record supports multiple table foreign key relationships with the restriction -that each relationship corresponds to a unique table. For example, the Players -table may only have one set of foreign key relationship with table Teams, it may -have other relationships that corresponds to other tables (including the Players table itself). +Since version 3.1.2, Active Record supports multiple foreign key +references of the same table. Ambiguity between multiple foreign key references to the same table is +resolved by providing the foreign key column name as the 3rd parameter in the relationship array. +For example, both the following foreign keys owner_id and reporter_id +references the same table defined in UserRecord. + +class TicketRecord extends TActiveRecord +{ + public $owner_id; + public $reporter_id; + + public $owner; + public $reporter; + + public static $RELATION=array + ( + 'owner' => array(self::BELONGS_TO, 'UserRecord', 'owner_id'), + 'reporter' => array(self::BELONGS_TO, 'UserRecord', 'reporter_id'), + ); +} + +This is applicable to relationships including BELONGS_TO, HAS_ONE and +HAS_MANY. See section Self Referenced Association Tables for solving ambiguity of MANY_TO_MANY +relationships.

The "has many" relationship is not fetched automatically when you use any of the Active Record finder methods. @@ -713,7 +733,7 @@ class PlayerRecord extends TActiveRecord public static $RELATIONS=array ( 'team' => array(self::BELONGS_TO, 'TeamRecord'), - 'skills' => array(self::HAS_MANY, 'SkillRecord', 'Player_Skills'), + 'skills' => array(self::MANY_TO_MANY, 'SkillRecord', 'Player_Skills'), 'profile' => array(self::HAS_ONE, 'ProfileRecord'), ); @@ -850,9 +870,9 @@ in the Player_Skills association table using an inner join.

The Prado Active Record design implements the two stage approach. For the -Players-Skills M-N (many-to-many) entity relationship, we need -to define a has many relationship in the PlayerRecord class and -in addition define a has many relationship in the SkillRecord class as well. +Players-Skills M-N (many-to-many) entity relationship, we +define a many-to-many relationship in the PlayerRecord class and +in addition we may define a many-to-many relationship in the SkillRecord class as well. The following sample code defines the complete SkillRecord class with a many-to-many relationship with the PlayerRecord class. (See the PlayerRecord class definition above to the corresponding many-to-many relationship with the SkillRecord class.) @@ -869,7 +889,7 @@ class SkillRecord extends TActiveRecord public static $RELATIONS=array ( - 'players' => array(self::HAS_MANY, 'PlayerRecord', 'Player_Skills'), + 'players' => array(self::MANY_TO_MANY, 'PlayerRecord', 'Player_Skills'), ); public static function finder($className=__CLASS__) @@ -882,12 +902,20 @@ class SkillRecord extends TActiveRecord

The static $RELATIONS property of SkillRecord defines that the property $players has many PlayerRecords via an association table 'Player_Skills'. -In array(self::HAS_MANY, 'PlayerRecord', 'Player_Skills'), the first element defines the -relationship type, in this case self::HAS_MANY, +In array(self::MANY_TO_MANY, 'PlayerRecord', 'Player_Skills'), the first element defines the +relationship type, in this case self::MANY_TO_MANY, the second element is a string 'PlayerRecord' that corresponds to the class name of the PlayerRecord class, and the third element is the name of the association table name.

+ +
Note: +Prior to version 3.1.2 (versions up to 3.1.1), the many-to-many relationship was +defined using self::HAS_MANY. For version 3.1.2 onwards, this must be changed +to self::MANY_TO_MANY. This can be done by searching for the HAS_MANY in your +source code and carfully changing the appropriate definitions. +
+

A list of player objects with the corresponding collection of skill objects may be fetched as follows.

@@ -951,7 +979,7 @@ class Item extends TActiveRecord public static $RELATIONS=array ( - 'related_items' => array(self::HAS_MANY, + 'related_items' => array(self::MANY_TO_MANY, 'Item', 'related_items.related_item_id'), ); } diff --git a/demos/quickstart/protected/pages/Database/ar_objects.png b/demos/quickstart/protected/pages/Database/ar_objects.png index 50ab812d..ac33b88b 100644 Binary files a/demos/quickstart/protected/pages/Database/ar_objects.png and b/demos/quickstart/protected/pages/Database/ar_objects.png differ diff --git a/demos/quickstart/protected/pages/Database/ar_objects.vsd b/demos/quickstart/protected/pages/Database/ar_objects.vsd index 10346c54..d3b3963d 100644 Binary files a/demos/quickstart/protected/pages/Database/ar_objects.vsd and b/demos/quickstart/protected/pages/Database/ar_objects.vsd differ diff --git a/demos/quickstart/protected/pages/Database/id/ActiveRecord.page b/demos/quickstart/protected/pages/Database/id/ActiveRecord.page index b7b9e612..7273640d 100644 --- a/demos/quickstart/protected/pages/Database/id/ActiveRecord.page +++ b/demos/quickstart/protected/pages/Database/id/ActiveRecord.page @@ -615,7 +615,7 @@ class PlayerRecord extends TActiveRecord public static $RELATIONS=array ( 'team' => array(self::BELONGS_TO, 'TeamRecord'), - 'skills' => array(self::HAS_MANY, 'SkillRecord', 'Player_Skills'), + 'skills' => array(self::MANY_TO_MANY, 'SkillRecord', 'Player_Skills'), 'profile' => array(self::HAS_ONE, 'ProfileRecord'), ); @@ -737,7 +737,7 @@ class SkillRecord extends TActiveRecord public static $RELATIONS=array ( - 'players' => array(self::HAS_MANY, 'PlayerRecord', 'Player_Skills'), + 'players' => array(self::MANY_TO_MANY, 'PlayerRecord', 'Player_Skills'), ); public static function finder($className=__CLASS__) @@ -749,7 +749,7 @@ class SkillRecord extends TActiveRecord

Properti statis $RELATIONS dari SkillRecord mendefinisikan bahwa properti $players memiliki banyak PlayerRecords melalui tabel asosiasi 'Player_Skills'. -Dalam array(self::HAS_MANY, 'PlayerRecord', 'Player_Skills'), elemen pertama mendefinisikan tipe hubungan, dalam hal ini self::HAS_MANY, +Dalam array(self::MANY_TO_MANY, 'PlayerRecord', 'Player_Skills'), elemen pertama mendefinisikan tipe hubungan, dalam hal ini self::HAS_MANY, elemen kedua adalah string 'PlayerRecord' yang terkait ke nama kelas dari kelas PlayerRecord, dan elemen ketiga adalah nama dari nama tabel asosiasi.

@@ -805,7 +805,7 @@ class Item extends TActiveRecord public static $RELATIONS=array ( - 'related_items' => array(self::HAS_MANY, + 'related_items' => array(self::MANY_TO_MANY, 'Item', 'related_items.related_item_id'), ); } diff --git a/framework/Caching/TXCache.php b/framework/Caching/TXCache.php new file mode 100644 index 00000000..698020af --- /dev/null +++ b/framework/Caching/TXCache.php @@ -0,0 +1,126 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2007 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $ + * @package System.Caching + */ + +/** + * TXCache class + * + * TXCache implements a cache application module based on {@link http://xcache.lighttpd.net/ xcache}. + * + * By definition, cache does not ensure the existence of a value + * even if it never expires. Cache is not meant to be an persistent storage. + * + * To use this module, the xcache PHP extension must be loaded and configured in the php.ini. + * + * Some usage examples of TXCache are as follows, + * + * $cache=new TXCache; // TXCache may also be loaded as a Prado application module + * $cache->init(null); + * $cache->add('object',$object); + * $object2=$cache->get('object'); + * + * + * If loaded, TXCache will register itself with {@link TApplication} as the + * cache module. It can be accessed via {@link TApplication::getCache()}. + * + * TXCache may be configured in application configuration file as follows + * + * + * + * + * @author Wei Zhuo + * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $ + * @package System.Caching + * @since 3.1.1 + */ +class TXCache extends TCache +{ + /** + * Initializes this module. + * This method is required by the IModule interface. + * @param TXmlElement configuration for this module, can be null + * @throws TConfigurationException if xcache extension is not installed or not started, check your php.ini + */ + public function init($config) + { + if(!function_exists('xcache_isset')) + throw new TConfigurationException('xcache_extension_required'); + + $enabled = intval(ini_get('xcache.cacher')) !== 0; + $var_size = intval(ini_get('xcache.var_size')); + + if(!($enabled && $var_size > 0)) + throw new TConfigurationException('xcache_extension_not_enabled'); + + parent::init($config); + } + + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string a unique key identifying the cached value + * @return string the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return xcache_isset($key) ? xcache_get($key) : false; + } + + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string the key identifying the value to be cached + * @param string the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key,$value,$expire) + { + return xcache_set($key,$value,$expire); + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string the key identifying the value to be cached + * @param string the value to be cached + * @param integer the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key,$value,$expire) + { + return !xcache_isset($key) ? $this->setValue($key,$value,$expire) : false; + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return xcache_unset($key); + } + + /** + * Deletes all values from cache. + * Be careful of performing this operation if the cache is shared by multiple applications. + */ + public function flush() + { + return xcache_clear_cache(); + } +} + +?> \ No newline at end of file diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php index d7278d64..d4ff07e5 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php @@ -77,8 +77,7 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $fkObject = $this->getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($this->getSourceRecord(),$fkObject); + $fkeys = $this->getRelationForeignKeys(); $properties = array_keys($fkeys); $fields = array_values($fkeys); @@ -87,6 +86,16 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation $fkObjects = $this->findForeignObjects($fields, $indexValues); $this->populateResult($results,$properties,$fkObjects,$fields); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($this->getSourceRecord(),$fkObject); + } /** * Sets the foreign objects to the given property on the source object. diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php index cbba3ebd..f7426862 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php @@ -1,4 +1,4 @@ -getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); + $fkeys = $this->getRelationForeignKeys(); $properties = array_values($fkeys); $fields = array_keys($fkeys); @@ -86,6 +85,16 @@ class TActiveRecordHasMany extends TActiveRecordRelation $this->populateResult($results,$properties,$fkObjects,$fields); } + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($fkObject, $this->getSourceRecord()); + } + /** * Updates the associated foreign objects. * @return boolean true if all update are success (including if no update was required), false otherwise . @@ -113,5 +122,5 @@ class TActiveRecordHasMany extends TActiveRecordRelation 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 0e176607..564d3d22 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php @@ -37,7 +37,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation'); * * public static $RELATIONS = array * ( - * 'Categories' => array(self::HAS_MANY, 'CategoryRecord', 'Article_Category') + * 'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category') * ); * * public static function finder($className=__CLASS__) @@ -54,7 +54,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation'); * * public static $RELATIONS = array * ( - * 'Articles' => array(self::HAS_MANY, 'ArticleRecord', 'Article_Category') + * 'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category') * ); * * public static function finder($className=__CLASS__) @@ -96,17 +96,22 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $association = $this->getAssociationTable(); - $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord()); - + list($sourceKeys, $foreignKeys) = $this->getRelationForeignKeys(); $properties = array_values($sourceKeys); - $indexValues = $this->getIndexValues($properties, $results); + $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys); + } + /** + * @return array 2 arrays of source keys and foreign keys from the association table. + */ + public function getRelationForeignKeys() + { + $association = $this->getAssociationTable(); + $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord(), true); $fkObject = $this->getContext()->getForeignRecordFinder(); $foreignKeys = $this->findForeignKeys($association, $fkObject); - - $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys); + return array($sourceKeys, $foreignKeys); } /** @@ -182,7 +187,7 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation */ protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys) { - $criteria = $this->getContext()->getCriteria(); + $criteria = $this->getCriteria(); $finder = $this->getContext()->getForeignRecordFinder(); $registry = $finder->getRecordManager()->getObjectStateRegistry(); $type = get_class($finder); @@ -198,7 +203,6 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation $collections[$hash][] = $obj; $registry->registerClean($obj); } - $this->setResultCollection($results, $collections, array_values($sourceKeys)); } diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php index b22428ae..e5a36659 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php @@ -92,9 +92,7 @@ class TActiveRecordHasOne extends TActiveRecordRelation */ protected function collectForeignObjects(&$results) { - $fkObject = $this->getContext()->getForeignRecordFinder(); - $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord()); - + $fkeys = $this->getRelationForeignKeys(); $properties = array_values($fkeys); $fields = array_keys($fkeys); @@ -102,6 +100,16 @@ class TActiveRecordHasOne extends TActiveRecordRelation $fkObjects = $this->findForeignObjects($fields,$indexValues); $this->populateResult($results,$properties,$fkObjects,$fields); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + $fkObject = $this->getContext()->getForeignRecordFinder(); + return $this->findForeignKeys($fkObject, $this->getSourceRecord()); + } /** * Sets the foreign objects to the given property on the source object. diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php index 5a3ea50e..8e9cc9b5 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php @@ -26,10 +26,12 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext'); abstract class TActiveRecordRelation { private $_context; + private $_criteria; - public function __construct(TActiveRecordRelationContext $context) + public function __construct(TActiveRecordRelationContext $context, $criteria) { $this->_context = $context; + $this->_criteria = $criteria; } /** @@ -39,6 +41,14 @@ abstract class TActiveRecordRelation { return $this->_context; } + + /** + * @return TActiveRecordCriteria + */ + protected function getCriteria() + { + return $this->_criteria; + } /** * @return TActiveRecord @@ -75,6 +85,16 @@ abstract class TActiveRecordRelation array_push($stack,$this); //call it later return $results; } + + /** + * Fetch results for current relationship. + * @return boolean always true. + */ + public function fetchResultsInto($obj) + { + $this->collectForeignObjects($obj); + return true; + } /** * Returns foreign keys in $fromRecord with source column names as key @@ -84,7 +104,7 @@ abstract class TActiveRecordRelation * @param TActiveRecord $matchesRecord * @return array foreign keys with source column names as key and foreign column names as value. */ - protected function findForeignKeys($from, $matchesRecord) + protected function findForeignKeys($from, $matchesRecord, $loose=false) { $gateway = $matchesRecord->getRecordGateway(); $matchingTableName = $gateway->getRecordTableInfo($matchesRecord)->getTableName(); @@ -93,13 +113,42 @@ abstract class TActiveRecordRelation $tableInfo = $gateway->getRecordTableInfo($from); foreach($tableInfo->getForeignKeys() as $fkeys) { - if($fkeys['table']===$matchingTableName) - return $fkeys['keys']; + if(strtolower($fkeys['table'])===strtolower($matchingTableName)) + { + if(!$loose && $this->getContext()->hasFkField()) + return $this->getFkFields($fkeys['keys']); + else + return $fkeys['keys']; + } } $matching = $gateway->getRecordTableInfo($matchesRecord)->getTableFullName(); throw new TActiveRecordException('ar_relations_missing_fk', $tableInfo->getTableFullName(), $matching); } + + /** + * @return array foreign key field names as key and object properties as value. + * @since 3.1.2 + */ + abstract public function getRelationForeignKeys(); + + /** + * Find matching foreign key fields from the 3rd element of an entry in TActiveRecord::$RELATION. + * Assume field names consist of [\w-] character sets. Prefix to the field names ending with a dot + * are ignored. + */ + private function getFkFields($fkeys) + { + $matching = array(); + preg_match_all('/\s*(\S+\.)?([\w-]+)\s*/', $this->getContext()->getFkField(), $matching); + $fields = array(); + foreach($fkeys as $fkName => $field) + { + if(in_array($fkName, $matching[2])) + $fields[$fkName] = $field; + } + return $fields; + } /** * @param mixed object or array to be hashed @@ -122,9 +171,8 @@ abstract class TActiveRecordRelation */ protected function findForeignObjects($fields, $indexValues) { - $criteria = $this->getContext()->getCriteria(); $finder = $this->getContext()->getForeignRecordFinder(); - return $finder->findAllByIndex($criteria, $fields, $indexValues); + return $finder->findAllByIndex($this->_criteria, $fields, $indexValues); } /** diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php index 189d2c5e..d83aa63a 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -32,13 +32,12 @@ class TActiveRecordRelationContext private $_property; private $_sourceRecord; - private $_criteria; - private $_relation; + private $_relation; //data from an entry of TActiveRecord::$RELATION + private $_fkeys; - public function __construct($source, $property=null, $criteria=null) + public function __construct($source, $property=null) { $this->_sourceRecord=$source; - $this->_criteria=$criteria; if($property!==null) list($this->_property, $this->_relation) = $this->getSourceRecordRelation($property); } @@ -48,7 +47,6 @@ class TActiveRecordRelationContext * property from the $RELATIONS static property in TActiveRecord. * @param string relation property name * @return array array($propertyName, $relation) relation definition. - * @throws TActiveRecordException if property is not defined or missing. */ protected function getSourceRecordRelation($property) { @@ -58,8 +56,6 @@ class TActiveRecordRelationContext if(strtolower($name)===$property) return array($name, $relation); } - throw new TActiveRecordException('ar_undefined_relation_prop', - $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); } /** @@ -71,6 +67,15 @@ class TActiveRecordRelationContext return $class->getStaticPropertyValue(self::RELATIONS_CONST); } + /** + * @return boolean true if the relation is defined in TActiveRecord::$RELATIONS + * @since 3.1.2 + */ + public function hasRecordRelation() + { + return $this->_relation!==null; + } + public function getPropertyValue() { $obj = $this->getSourceRecord(); @@ -85,14 +90,6 @@ class TActiveRecordRelationContext return $this->_property; } - /** - * @return TActiveRecordCriteria sql query criteria for fetching the related record. - */ - public function getCriteria() - { - return $this->_criteria; - } - /** * @return TActiveRecord the active record instance that queried for its related records. */ @@ -109,6 +106,18 @@ class TActiveRecordRelationContext return $this->_relation[1]; } + /** + * @return array foreign key of this relations, the keys is dependent on the + * relationship type. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + if($this->_fkeys===null) + $this->_fkeys=$this->getRelationHandler()->getRelationForeignKeys(); + return $this->_fkeys; + } + /** * @return string HAS_MANY, HAS_ONE, or BELONGS_TO */ @@ -117,6 +126,25 @@ class TActiveRecordRelationContext return $this->_relation[0]; } + /** + * @return string foreign key field names, comma delimited. + * @since 3.1.2 + */ + public function getFkField() + { + return $this->_relation[2]; + } + + /** + * @return boolean true if the 3rd element of an TActiveRecord::$RELATION entry is set. + * @since 3.1.2 + */ + public function hasFkField() + { + $notManyToMany = $this->getRelationType() !== TActiveRecord::MANY_TO_MANY; + return $notManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); + } + /** * @return string the M-N relationship association table name. */ @@ -130,7 +158,8 @@ class TActiveRecordRelationContext */ public function hasAssociationTable() { - return isset($this->_relation[2]); + $isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY; + return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); } /** @@ -145,29 +174,33 @@ class TActiveRecordRelationContext * Creates and return the TActiveRecordRelation handler for specific relationships. * An instance of TActiveRecordHasOne, TActiveRecordBelongsTo, TActiveRecordHasMany, * or TActiveRecordHasManyAssocation will be returned. + * @param TActiveRecordCriteria search criteria * @return TActiveRecordRelation record relationship handler instnace. + * @throws TActiveRecordException if property is not defined or missing. */ - public function getRelationHandler() + public function getRelationHandler($criteria=null) { + if(!$this->hasRecordRelation()) + { + throw new TActiveRecordException('ar_undefined_relation_prop', + $property, get_class($this->_sourceRecord), self::RELATIONS_CONST); + } + if($criteria===null) + $criteria = new TActiveRecordCriteria; 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); - } + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); + return new TActiveRecordHasMany($this, $criteria); + case TActiveRecord::MANY_TO_MANY: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); + return new TActiveRecordHasManyAssociation($this, $criteria); case TActiveRecord::HAS_ONE: Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne'); - return new TActiveRecordHasOne($this); + return new TActiveRecordHasOne($this, $criteria); case TActiveRecord::BELONGS_TO: Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo'); - return new TActiveRecordBelongsTo($this); + return new TActiveRecordBelongsTo($this, $criteria); default: throw new TActiveRecordException('ar_invalid_relationship'); } diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php index b53cdf45..446c87e4 100644 --- a/framework/Data/ActiveRecord/TActiveRecord.php +++ b/framework/Data/ActiveRecord/TActiveRecord.php @@ -94,9 +94,10 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext'); */ abstract class TActiveRecord extends TComponent { - const HAS_MANY='HAS_MANY'; const BELONGS_TO='BELONGS_TO'; const HAS_ONE='HAS_ONE'; + const HAS_MANY='HAS_MANY'; + const MANY_TO_MANY='MANY_TO_MANY'; private static $_columnMapping=array(); @@ -110,6 +111,8 @@ abstract class TActiveRecord extends TComponent */ public static $COLUMN_MAPPING=array(); + private static $_relations=array(); + /** * This static variable defines the relationships. * The keys are public variable/property names defined in the AR class. @@ -562,15 +565,77 @@ abstract class TActiveRecord extends TComponent /** * Returns the active record relationship handler for $RELATION with key * value equal to the $property value. - * @param string relationship property name. + * @param string relationship/property name corresponding to keys in $RELATION array. * @param array method call arguments. * @return TActiveRecordRelation */ - protected function getRelationHandler($property,$args) + protected function getRelationHandler($property,$args=array()) { $criteria = $this->getCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1)); - $context = new TActiveRecordRelationContext($this, $property, $criteria); - return $context->getRelationHandler(); + return $this->getRelationContext($property)->getRelationHandler($criteria); + } + + /** + * Gets a static copy of the relationship context for given property (a key + * in $RELATION), returns null if invalid relationship. Keeps a null + * reference to all invalid relations called. + * @param string relationship/property name corresponding to keys in $RELATION array. + * @return TActiveRecordRelationContext object containing information on + * the active record relationships for given property, null if invalid relationship + * @since 3.1.2 + */ + protected function getRelationContext($property) + { + $prop = get_class($this).':'.$property; + if(!isset(self::$_relations[$prop])) + { + $context = new TActiveRecordRelationContext($this, $property); + //keep a null reference to all non-existing relations called? + self::$_relations[$prop] = $context->hasRecordRelation() ? $context : null; + } + return self::$_relations[$prop]; + } + + /** + * Tries to load the relationship results for the given property. The $property + * value should correspond to an entry key in the $RELATION array. + * This method can be used to lazy load relationships. + * + * class TeamRecord extends TActiveRecord + * { + * ... + * + * private $_players; + * public static $RELATION=array + * ( + * 'players' => array(self::HAS_MANY, 'PlayerRecord'), + * ); + * + * public function setPlayers($array) + * { + * $this->_players=$array; + * } + * + * public function getPlayers() + * { + * if($this->_players===null) + * $this->fetchResultsFor('players'); + * return $this->_players; + * } + * } + * Usage example: + * $team = TeamRecord::finder()->findByPk(1); + * var_dump($team->players); //uses lazy load to fetch 'players' relation + * + * @param string relationship/property name corresponding to keys in $RELATION array. + * @return boolean true if relationship exists, false otherwise. + * @since 3.1.2 + */ + protected function fetchResultsFor($property) + { + if( ($context=$this->getRelationContext($property)) !== null) + return $context->getRelationHandler()->fetchResultsInto($this); + return false; } /** diff --git a/tests/simple_unit/ActiveRecord/ForeignKeyTestCase.php b/tests/simple_unit/ActiveRecord/ForeignKeyTestCase.php index cbde7fa0..2e4bee2d 100644 --- a/tests/simple_unit/ActiveRecord/ForeignKeyTestCase.php +++ b/tests/simple_unit/ActiveRecord/ForeignKeyTestCase.php @@ -26,7 +26,7 @@ class Album extends SqliteRecord public static $RELATIONS = array( 'Tracks' => array(self::HAS_MANY, 'Track'), - 'Artists' => array(self::HAS_MANY, 'Artist', 'album_artists'), + 'Artists' => array(self::MANY_TO_MANY, 'Artist', 'album_artists'), 'cover' => array(self::HAS_ONE, 'Cover') ); @@ -42,8 +42,9 @@ class Artist extends SqliteRecord public $Albums = array(); - public static $RELATIONS=array( - 'Albums' => array(self::HAS_MANY, 'Album', 'album_artists') + public static $RELATIONS=array + ( + 'Albums' => array(self::MANY_TO_MANY, 'Album', 'album_artists') ); public static function finder($class=__CLASS__) @@ -158,6 +159,7 @@ class ForeignKeyTestCase extends UnitTestCase function test_self_reference_fk() { $item = ItemRecord::finder()->withRelated_Items()->findByPk(1); + $this->assertNotNull($item); $this->assertEqual($item->name, "Professional Work Attire"); @@ -168,6 +170,7 @@ class ForeignKeyTestCase extends UnitTestCase $this->assertEqual($item->related_items[1]->name, "Grooming and Hygiene"); $this->assertEqual($item->related_items[1]->item_id, 3); } + } ?> \ No newline at end of file diff --git a/tests/simple_unit/ActiveRecord/ForeignObjectUpdateTest.php b/tests/simple_unit/ActiveRecord/ForeignObjectUpdateTest.php index 36864f77..026efb4d 100644 --- a/tests/simple_unit/ActiveRecord/ForeignObjectUpdateTest.php +++ b/tests/simple_unit/ActiveRecord/ForeignObjectUpdateTest.php @@ -52,7 +52,7 @@ class PlayerRecord extends BaseFkRecord public static $RELATIONS=array ( - 'skills' => array(self::HAS_MANY, 'SkillRecord', 'player_skills'), + 'skills' => array(self::MANY_TO_MANY, 'SkillRecord', 'player_skills'), 'team' => array(self::BELONGS_TO, 'TeamRecord'), 'profile' => array(self::HAS_ONE, 'ProfileRecord'), ); @@ -112,7 +112,7 @@ class SkillRecord extends BaseFkRecord public static $RELATIONS=array ( - 'players' => array(self::HAS_MANY, 'PlayerRecord', 'player_skills'), + 'players' => array(self::MANY_TO_MANY, 'PlayerRecord', 'player_skills'), ); public static function finder($className=__CLASS__) @@ -236,6 +236,7 @@ class ForeignObjectUpdateTest extends UnitTestCase $this->assertEqual($player4->skills[1]->name, 'Skip'); $this->assertEqual($player4->skills[2]->name, 'Push'); } +//*/ } ?> \ No newline at end of file diff --git a/tests/simple_unit/ActiveRecord/MultipleForeignKeyTestCase.php b/tests/simple_unit/ActiveRecord/MultipleForeignKeyTestCase.php new file mode 100644 index 00000000..16036b9f --- /dev/null +++ b/tests/simple_unit/ActiveRecord/MultipleForeignKeyTestCase.php @@ -0,0 +1,179 @@ + array(self::BELONGS_TO, 'Table2', 'fk1'), + 'object2' => array(self::BELONGS_TO, 'Table2', 'fk2'), + 'object3' => array(self::BELONGS_TO, 'Table2', 'fk3'), + ); + + public static function finder($class=__CLASS__) + { + return parent::finder($class); + } +} + +/** + * CREATE TABLE table2 (id integer PRIMARY KEY AUTOINCREMENT,field1 varchar) + */ +class Table2 extends MultipleFKSqliteRecord +{ + public $id; + public $field1; + + private $_state1; + public $state2; + public $state3; + + public static $RELATIONS = array + ( + 'state1' => array(self::HAS_MANY, 'Table1', 'fk1'), + 'state2' => array(self::HAS_MANY, 'Table1', 'fk2'), + 'state3' => array(self::HAS_ONE, 'Table1', 'fk3'), + ); + + public function setState1($obj) + { + $this->_state1 = $obj; + } + + public function getState1() + { + if(is_null($this->_state1)) + $this->fetchResultsFor('state1'); + return $this->_state1; + } + + public static function finder($class=__CLASS__) + { + return parent::finder($class); + } +} + + +class Category extends MultipleFKSqliteRecord +{ + public $cat_id; + public $category_name; + public $parent_cat; + + public $parent_category; + public $child_categories=array(); + + public static $RELATIONS=array + ( + 'parent_category' => array(self::BELONGS_TO, 'Category'), + 'child_categories' => array(self::HAS_MANY, 'Category'), + ); + + public static function finder($class=__CLASS__) + { + return parent::finder($class); + } +} + +class MultipleForeignKeyTestCase extends UnitTestCase +{ + function testBelongsTo() + { + $obj = Table1::finder()->withObject1()->findAll(); + $this->assertEqual(count($obj), 3); + $this->assertEqual($obj[0]->id, '1'); + $this->assertEqual($obj[1]->id, '2'); + $this->assertEqual($obj[2]->id, '3'); + + $this->assertEqual($obj[0]->object1->id, '1'); + $this->assertEqual($obj[1]->object1->id, '2'); + $this->assertEqual($obj[2]->object1->id, '2'); + } + + function testHasMany() + { + $obj = Table2::finder()->withState1()->findAll(); + $this->assertEqual(count($obj), 5); + + $this->assertEqual(count($obj[0]->state1), 1); + $this->assertEqual($obj[0]->state1[0]->id, '1'); + + $this->assertEqual(count($obj[1]->state1), 2); + $this->assertEqual($obj[1]->state1[0]->id, '2'); + $this->assertEqual($obj[1]->state1[1]->id, '3'); + + $this->assertEqual(count($obj[2]->state1), 0); + $this->assertEqual($obj[2]->id, '3'); + + $this->assertEqual(count($obj[3]->state1), 0); + $this->assertEqual($obj[3]->id, '4'); + } + + function testHasOne() + { + $obj = Table2::finder()->withState3('id = 3')->findAll(); + + $this->assertEqual(count($obj), 5); + + $this->assertEqual($obj[0]->id, '1'); + $this->assertNull($obj[0]->state3); + + $this->assertEqual($obj[1]->id, '2'); + $this->assertNull($obj[1]->state3); + + $this->assertEqual($obj[2]->id, '3'); + $this->assertNotNull($obj[2]->state3); + $this->assertEqual($obj[2]->state3->id, '3'); + + $this->assertEqual($obj[3]->id, '4'); + $this->assertNull($obj[3]->state3); + } + + function testParentChild() + { + $obj = Category::finder()->withChild_Categories()->withParent_Category()->findByPk(2); + + $this->assertEqual($obj->cat_id, '2'); + $this->assertEqual(count($obj->child_categories), 2); + $this->assertNotNull($obj->parent_category); + + $this->assertEqual($obj->child_categories[0]->cat_id, 3); + $this->assertEqual($obj->child_categories[1]->cat_id, 4); + + $this->assertEqual($obj->parent_category->cat_id, 1); + } +} + +?> \ No newline at end of file diff --git a/tests/simple_unit/ActiveRecord/records/ItemRecord.php b/tests/simple_unit/ActiveRecord/records/ItemRecord.php index 45d15427..52a1a658 100644 --- a/tests/simple_unit/ActiveRecord/records/ItemRecord.php +++ b/tests/simple_unit/ActiveRecord/records/ItemRecord.php @@ -21,7 +21,7 @@ class ItemRecord extends TActiveRecord public static $RELATIONS=array ( - 'related_items' => array(self::HAS_MANY, 'ItemRecord', 'related_items.(related_item_id)'), + 'related_items' => array(self::MANY_TO_MANY, 'ItemRecord', 'related_items.related_item_id'), ); public function getDbConnection() diff --git a/tests/simple_unit/ActiveRecord/test1.sqlite b/tests/simple_unit/ActiveRecord/test1.sqlite new file mode 100644 index 00000000..1e056b52 Binary files /dev/null and b/tests/simple_unit/ActiveRecord/test1.sqlite differ diff --git a/tests/test_tools/simpletest/test_case.php b/tests/test_tools/simpletest/test_case.php index bc215640..bc4f0c42 100644 --- a/tests/test_tools/simpletest/test_case.php +++ b/tests/test_tools/simpletest/test_case.php @@ -240,7 +240,8 @@ 'Unexpected exception of type [' . get_class($exception) . '] with message ['. $exception->getMessage() . '] in ['. $exception->getFile() . - '] line [' . $exception->getLine() . ']'); + '] line [' . $exception->getLine() . + '] stack [' . $exception->getTraceAsString() .']'); } /** -- cgit v1.2.3