diff options
6 files changed, 200 insertions, 83 deletions
| @@ -5,6 +5,7 @@ CHG: Changed TConditional so that the controls in its template behave like they  CHG: Active Record many-to-many relationship change from self::HAS_MANY to self::MANY_TO_MANY (Wei)  ENH: Active Record supports multiple foreign references of the same table (Wei)  ENH: Added TDbCommand.queryColumn() (Qiang) +ENH: Active Record now supports implicitly declared related properties (Qiang)  NEW: Added TDbLogRoute (Qiang)  NEW: Added TDataRenderer and TItemDataRenderer (Qiang)  NEW: Ticket#544 - Added TXCache (Wei) diff --git a/demos/quickstart/protected/pages/Database/ActiveRecord.page b/demos/quickstart/protected/pages/Database/ActiveRecord.page index cb10d184..d2793fa1 100644 --- a/demos/quickstart/protected/pages/Database/ActiveRecord.page +++ b/demos/quickstart/protected/pages/Database/ActiveRecord.page @@ -624,7 +624,7 @@ class TeamRecord extends TActiveRecord      public $name;      public $location; -    public $players=array(); +    public $players=array();  // this declaration is no longer needed since v3.1.2      //define the $player member having has many relationship with PlayerRecord      public static $RELATIONS=array @@ -650,6 +650,16 @@ The second element is a string <tt>'PlayerRecord'</tt> that corresponds to the  class name of the <tt>PlayerRecord</tt> class.  </p> +<div class="note"><b class="note">Note:</b> +As described in the code comment above, since version <b>3.1.2</b>, related properties no longer +need to be explicitly declared. By default, they will be implicitly declared according to +keys of the <tt>$RELATIONS</tt> array. A major benefit of declared related properties implicitly +is that related objects can be automatically loaded in a lazy way. For example, assume we have +a <tt>TeamRecord</tt> instance <tt>$team</tt>. We can access the players via <tt>$team->players</tt>, +even if we have never issued fetch command for players. If <tt>$players</tt> is explicitly declared, +we will have to use the <tt>with</tt> approach described in the following to fetch the player records. +</div> +  <p id="710022" class="block-content">  The foreign key constraint of the <tt>Players</tt> table is used to determine the corresponding  <tt>Teams</tt> table's corresponding key names. This is done automatically handled @@ -657,9 +667,9 @@ in Active Record by inspecting the <tt>Players</tt> and <tt>Teams</tt> table def  </p>  <div class="info"><b class="note">Info:</b> -Since version <b>3.1.2</b>, Active Record supports multiple foreign key  +Since version <b>3.1.2</b>, 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.  +resolved by providing the foreign key column name as the 3rd parameter in the relationship array.  For example, both the following foreign keys <tt>owner_id</tt> and <tt>reporter_id</tt>  references the same table defined in <tt>UserRecord</tt>.  <com:TTextHighlighter Language="php" CssClass="source block-content"> @@ -668,8 +678,8 @@ class TicketRecord extends TActiveRecord       public $owner_id;  	 public $reporter_id; -     public $owner; -	 public $reporter; +     public $owner;     // this declaration is no longer needed since v3.1.2 +	 public $reporter;  // this declaration is no longer needed since v3.1.2  	 public static $RELATION=array  	 ( @@ -679,7 +689,7 @@ class TicketRecord extends TActiveRecord  }  </com:TTextHighlighter>  This is applicable to relationships including <tt>BELONGS_TO</tt>, <tt>HAS_ONE</tt> and -<tt>HAS_MANY</tt>. See section <a href="#142021">Self Referenced Association Tables</a> for solving ambiguity of <tt>MANY_TO_MANY</tt>  +<tt>HAS_MANY</tt>. See section <a href="#142021">Self Referenced Association Tables</a> for solving ambiguity of <tt>MANY_TO_MANY</tt>  relationships.  </div> @@ -709,6 +719,16 @@ and other join conditions are not feasible using Active Records. For queries out  scope of Active Record the <a href="?page=Database.SqlMap">SqlMap Data Mapper</a> may be considered.  </div> +<div class="info"><b class="info">Info:</b> +The above <tt>with</tt> approach also works with implicitly declared related properties (introduced +in version 3.1.2). So what is the difference between the <tt>with</tt> approach and the lazy loading +approach? Lazy loading means we issue an SQL query if a related object is initially accessed and not ready, +while the <tt>with</tt> approach queries for the related objects once for all, no matter the related objects +are accessed or not. The lazy loading approach is very convenient since we do not need to explictly +load the related objects, while the <tt>with</tt> approach is more efficient if multiple records are +returned, each with some related objects. +</div> +  <h3 id="142018">Belongs To Relationship</h3>  <p id="710025" class="block-content">The "has many" relationship in the above section defines a collection of foreign  objects. In particular, we have that a <tt>TeamRecord</tt> has many (zero or more) @@ -726,9 +746,9 @@ class PlayerRecord extends TActiveRecord      public $age;      public $team_name; -    public $team; -    public $skills=array(); -    public $profile; +    public $team;              // this declaration is no longer needed since v3.1.2 +    public $skills=array();    // this declaration is no longer needed since v3.1.2 +    public $profile;           // this declaration is no longer needed since v3.1.2      public static $RELATIONS=array      ( @@ -785,7 +805,7 @@ class ProfileRecord extends TActiveRecord      public $player_id;      public $salary; -    public $player; +    public $player;  // this declaration is no longer needed since v3.1.2      public static $RELATIONS=array      ( @@ -830,8 +850,8 @@ class Category extends TActiveRecord      public $category_name;      public $parent_cat_id; -    public $parent_category; -    public $child_categories=array(); +    public $parent_category;            // this declaration is no longer needed since v3.1.2 +    public $child_categories=array();   // this declaration is no longer needed since v3.1.2      public static $RELATIONS=array      ( @@ -870,7 +890,7 @@ in the <tt>Player_Skills</tt> association table using an inner join.  </p>  <p id="710035" class="block-content">The Prado Active Record design implements the two stage approach. For the -<tt>Players</tt>-<tt>Skills</tt> M-N (many-to-many) entity relationship, we  +<tt>Players</tt>-<tt>Skills</tt> M-N (many-to-many) entity relationship, we  define a <b>many-to-many</b> relationship in the <tt>PlayerRecord</tt> class and  in addition we may define a <b>many-to-many</b> relationship in the <tt>SkillRecord</tt> class as well.  The following sample code defines the complete <tt>SkillRecord</tt> class with a @@ -885,7 +905,7 @@ class SkillRecord extends TActiveRecord      public $skill_id;      public $name; -    public $players=array(); +    public $players=array();    // this declaration is no longer needed since v3.1.2      public static $RELATIONS=array      ( @@ -910,10 +930,10 @@ of the association table name.  </p>  <div class="note"><b class="note">Note:</b> -Prior to version <b>3.1.2</b> (versions up to 3.1.1), the many-to-many relationship was  +Prior to version <b>3.1.2</b> (versions up to 3.1.1), the many-to-many relationship was  defined using <tt>self::HAS_MANY</tt>. For version <b>3.1.2</b> onwards, this must be changed  to <tt>self::MANY_TO_MANY</tt>. This can be done by searching for the <tt>HAS_MANY</tt> in your -source code and carfully changing the appropriate definitions.  +source code and carfully changing the appropriate definitions.  </div>  <p id="710037" class="block-content"> @@ -975,7 +995,7 @@ class Item extends TActiveRecord      //additional foreign item id defined in the association table      public $related_item_id; -    public $related_items=array(); +    public $related_items=array();    // this declaration is no longer needed since v3.1.2      public static $RELATIONS=array      ( @@ -1030,6 +1050,13 @@ PlayerSkillAssocation::finder()->deleteByPk(array('fk1','fk2'));  </com:TTextHighlighter>  <h2 id="142015">Lazy Loading Related Objects</h2> + +<div class="note"><b class="note">Note:</b> +Implicitly declared related properties introduced in version 3.1.2 automatically have lazy +loading feature. Therefore, the lazy loading technique described in the following is no longer +needed in most of the cases, unless you want to manipulate the related objects through getter/setter. +</div> +  <p id="710045" class="block-content">Using the <tt>with_xxx()</tt> methods will load the relationship record on demand. Retrieving the  related record using lazy loading (that is, only when those related objects are accessed) can be  achieved by using a feature of the <tt>TComponent</tt> that provides accessor methods. In particular, diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php index d4ff07e5..805739c1 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php @@ -81,12 +81,11 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation  		$properties = array_keys($fkeys);
  		$fields = array_values($fkeys);
 -
  		$indexValues = $this->getIndexValues($properties, $results);
  		$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
 @@ -94,7 +93,7 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation  	public function getRelationForeignKeys()
  	{
  		$fkObject = $this->getContext()->getForeignRecordFinder();
 -		return $this->findForeignKeys($this->getSourceRecord(),$fkObject);		
 +		return $this->findForeignKeys($this->getSourceRecord(),$fkObject);
  	}
  	/**
 @@ -110,7 +109,7 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation  		{
  			if(count($collections[$hash]) > 1)
  				throw new TActiveRecordException('ar_belongs_to_multiple_result');
 -			$source->setColumnValue($prop, $collections[$hash][0]);
 +			$source->$prop=$collections[$hash][0];
  		}
  	}
 diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php index 8e9cc9b5..5bde4898 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php @@ -41,7 +41,7 @@ abstract class TActiveRecordRelation  	{
  		return $this->_context;
  	}
 -	
 +
  	/**
  	 * @return TActiveRecordCriteria
  	 */
 @@ -58,6 +58,8 @@ abstract class TActiveRecordRelation  		return $this->getContext()->getSourceRecord();
  	}
 +	abstract protected function collectForeignObjects(&$results);
 +
  	/**
  	 * Dispatch the method calls to the source record finder object. When
  	 * an instance of TActiveRecord or an array of TActiveRecord is returned
 @@ -85,7 +87,7 @@ abstract class TActiveRecordRelation  			array_push($stack,$this); //call it later
  		return $results;
  	}
 -	
 +
  	/**
  	 * Fetch results for current relationship.
  	 * @return boolean always true.
 @@ -125,13 +127,13 @@ abstract class TActiveRecordRelation  		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	 
 +	 * @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
 @@ -238,7 +240,7 @@ abstract class TActiveRecordRelation  	{
  		$hash = $this->getObjectHash($source, $properties);
  		$prop = $this->getContext()->getProperty();
 -		$source->setColumnValue($prop, isset($collections[$hash]) ? $collections[$hash] : array());
 +		$source->$prop=isset($collections[$hash]) ? $collections[$hash] : array();
  	}
  }
 diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php index d83aa63a..8bc4362f 100644 --- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php +++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -25,46 +25,16 @@   */
  class TActiveRecordRelationContext
  {
 -	/**
 -	 * static property name in TActiveRecord that defines the record relationships.
 -	 */
 -	const RELATIONS_CONST = 'RELATIONS';
 -
  	private $_property;
 -	private $_sourceRecord;
 +	private $_record;
  	private $_relation; //data from an entry of TActiveRecord::$RELATION
  	private $_fkeys;
 -	public function __construct($source, $property=null)
 -	{
 -		$this->_sourceRecord=$source;
 -		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 array($propertyName, $relation) relation definition.
 -	 */
 -	protected function getSourceRecordRelation($property)
 -	{
 -		$property = strtolower($property);
 -		foreach($this->getRecordRelationships() as $name => $relation)
 -		{
 -			if(strtolower($name)===$property)
 -				return array($name, $relation);
 -		}
 -	}
 -
 -	/**
 -	 * @return array the key and values of TActiveRecord::$RELATIONS
 -	 */
 -	public function getRecordRelationships()
 +	public function __construct($record, $property=null, $relation=null)
  	{
 -		$class = new ReflectionClass($this->_sourceRecord);
 -		return $class->getStaticPropertyValue(self::RELATIONS_CONST);
 +		$this->_record=$record;
 +		$this->_property=$property;
 +		$this->_relation=$relation;
  	}
  	/**
 @@ -95,7 +65,7 @@ class TActiveRecordRelationContext  	 */
  	public function getSourceRecord()
  	{
 -		return $this->_sourceRecord;
 +		return $this->_record;
  	}
  	/**
 @@ -183,7 +153,7 @@ class TActiveRecordRelationContext  		if(!$this->hasRecordRelation())
  		{
  			throw new TActiveRecordException('ar_undefined_relation_prop',
 -				$property, get_class($this->_sourceRecord), self::RELATIONS_CONST);
 +				$this->_property, get_class($this->_record), 'RELATIONS');
  		}
  		if($criteria===null)
  			$criteria = new TActiveRecordCriteria;
 @@ -212,7 +182,7 @@ class TActiveRecordRelationContext  	public function updateAssociatedRecords($updateBelongsTo=false)
  	{
  		$success=true;
 -		foreach($this->getRecordRelationships() as $property=>$relation)
 +		foreach($this->_record->getRelations() as $property=>$relation)
  		{
  			$belongsTo = $relation[0]==TActiveRecord::BELONGS_TO;
  			if(($updateBelongsTo && $belongsTo) || (!$updateBelongsTo && !$belongsTo))
 @@ -220,7 +190,7 @@ class TActiveRecordRelationContext  				$obj = $this->getSourceRecord();
  				if(!$this->isEmptyFkObject($obj->getColumnValue($property)))
  				{
 -					$context = new self($this->getSourceRecord(),$property);
 +					$context = new TActiveRecordRelationContext($this->getSourceRecord(),$property,$relation);
  					$success = $context->getRelationHandler()->updateAssociatedRecords() && $success;
  				}
  			}
 diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php index f28b81e4..a3265dc8 100644 --- a/framework/Data/ActiveRecord/TActiveRecord.php +++ b/framework/Data/ActiveRecord/TActiveRecord.php @@ -87,6 +87,27 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');   * By using column mapping, we can regularize the naming convention of column names   * in active record.   * + * Since v3.1.2, TActiveRecord enhanced its support to access of foreign objects. + * By declaring a public static variable RELATIONS like the following, one can access + * the corresponding foreign objects easily: + * <code> + * class UserRecord extends TActiveRecord + * { + *     const TABLE='users'; + *     public static $RELATIONS=array + *     ( + *         'department'=>array(self::BELONGS_TO, 'DepartmentRecord', 'department_id'), + *         'contacts'=>array(self::HAS_MANY, 'ContactRecord', 'user_id'), + *     ); + * } + * </code> + * In the above, the users table is related with departments table (represented by + * DepartmentRecord) and contacts table (represented by ContactRecord). Now, given a UserRecord + * instance $user, one can access its department and contacts simply by: $user->department and + * $user->contacts. No explicit data fetching is needed. Internally, the foreign objects are + * fetched in a lazy way, which avoids unnecessary overhead if the foreign objects are not accessed + * at all. + *   * @author Wei Zhuo <weizho[at]gmail[dot]com>   * @version $Id$   * @package System.Data.ActiveRecord @@ -99,8 +120,6 @@ abstract class TActiveRecord extends TComponent  	const HAS_MANY='HAS_MANY';  	const MANY_TO_MANY='MANY_TO_MANY'; -	private static $_columnMapping=array(); -  	/**  	 * This static variable defines the column mapping.  	 * The keys are physical column names as defined in database, @@ -110,8 +129,7 @@ abstract class TActiveRecord extends TComponent  	 * @since 3.1.1  	 */  	public static $COLUMN_MAPPING=array(); - -	private static $_relations=array(); +	private static $_columnMapping=array();  	/**  	 * This static variable defines the relationships. @@ -121,6 +139,7 @@ abstract class TActiveRecord extends TComponent  	 * @since 3.1.1  	 */  	public static $RELATIONS=array(); +	private static $_relations=array();  	/**  	 * @var boolean true if this class is read only. @@ -133,6 +152,12 @@ abstract class TActiveRecord extends TComponent  	private $_connection;  	/** +	 * @var array list of foreign objects. +	 * @since 3.1.2 +	 */ +	private $_foreignObjects=array(); + +	/**  	 * Prevent __call() method creating __sleep() when serializing.  	 */  	public function __sleep() @@ -146,6 +171,7 @@ abstract class TActiveRecord extends TComponent  	public function __wake()  	{  		$this->setupColumnMapping(); +		$this->setupRelations();  	}  	/** @@ -161,6 +187,49 @@ abstract class TActiveRecord extends TComponent  		if($connection!==null)  			$this->_connection=$connection;  		$this->setupColumnMapping(); +		$this->setupRelations(); +	} + +	/** +	 * Magic method for reading properties. +	 * This method is overriden to provide read access to the foreign objects via +	 * the key names declared in the RELATIONS array. +	 * @param string property name +	 * @return mixed property value. +	 * @since 3.1.2 +	 */ +	public function __get($name) +	{ +		if($this->hasRelation($name) && !$this->canGetProperty($name)) +		{ +			$name2=strtolower($name); +			if(!isset($this->_foreignObjects[$name2])) +				$this->fetchResultsFor($name2); +			if(isset($this->_foreignObjects[$name2])) +				return $this->_foreignObjects[$name2]===false?null:$this->_foreignObjects[$name2]; +			else +			{ +				$this->_foreignObjects[$name2]=false; +				return null; +			} +		} +		return parent::__get($name); +	} + +	/** +	 * Magic method for writing properties. +	 * This method is overriden to provide write access to the foreign objects via +	 * the key names declared in the RELATIONS array. +	 * @param string property name +	 * @param mixed property value. +	 * @since 3.1.2 +	 */ +	public function __set($name,$value) +	{ +		if($this->hasRelation($name) && !$this->canSetProperty($name)) +			$this->_foreignObjects[strtolower($name)]=$value===null?false:$value; +		else +			parent::__set($name,$value);  	}  	/** @@ -177,6 +246,22 @@ abstract class TActiveRecord extends TComponent  	}  	/** +	 * @since 3.1.2 +	 */ +	private function setupRelations() +	{ +		$className=get_class($this); +		if(!isset(self::$_relations[$className])) +		{ +			$class=new ReflectionClass($className); +			$relations=array(); +			foreach($class->getStaticPropertyValue('RELATIONS') as $key=>$value) +				$relations[strtolower($key)]=$value; +			self::$_relations[$className]=$relations; +		} +	} + +	/**  	 * Copies data from an array or another object.  	 * @throws TActiveRecordException if data is not array or not object.  	 * @return TActiveRecord current instance. @@ -569,17 +654,22 @@ abstract class TActiveRecord extends TComponent  	 * value equal to the $property value.  	 * @param string relationship/property name corresponding to keys in $RELATION array.  	 * @param array method call arguments. -	 * @return TActiveRecordRelation +	 * @return TActiveRecordRelation, null if the context or the handler doesn't exist  	 */  	protected function getRelationHandler($property,$args=array())  	{ -		$criteria = $this->getCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1)); -		return $this->getRelationContext($property)->getRelationHandler($criteria); +		if(($context=$this->getRelationContext($property)) !== null) +		{ +			$criteria = $this->getCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1)); +			return $context->getRelationHandler($criteria); +		} +		else +			return null;  	}  	/**  	 * Gets a static copy of the relationship context for given property (a key -	 * in $RELATION), returns null if invalid relationship. Keeps a null +	 * in $RELATIONS), 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 @@ -588,14 +678,10 @@ abstract class TActiveRecord extends TComponent  	 */  	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]; +		if(($relation=$this->getRelation($property))!==null) +			return new TActiveRecordRelationContext($this,$property,$relation); +		else +			return null;  	}  	/** @@ -637,7 +723,8 @@ abstract class TActiveRecord extends TComponent  	{  		if( ($context=$this->getRelationContext($property)) !== null)  			return $context->getRelationHandler()->fetchResultsInto($this); -		return false; +		else +			return false;  	}  	/** @@ -780,5 +867,36 @@ abstract class TActiveRecord extends TComponent  			$columnName=self::$_columnMapping[$className][$columnName];  		$this->$columnName=$value;  	} + +	/** +	 * @param string relation property name +	 * @return array relation definition for the specified property +	 * @since 3.1.2 +	 */ +	public function getRelation($property) +	{ +		$className=get_class($this); +		$property=strtolower($property); +		return isset(self::$_relations[$className][$property])?self::$_relations[$className][$property]:null; +	} + +	/** +	 * @return array all relation definitions declared in the AR class +	 * @since 3.1.2 +	 */ +	public function getRelations() +	{ +		return self::$_relations[get_class($this)]; +	} + +	/** +	 * @param string AR property name +	 * @return boolean whether a relation is declared for the specified AR property +	 * @since 3.1.2 +	 */ +	public function hasRelation($property) +	{ +		return isset(self::$_relations[get_class($this)][strtolower($property)]); +	}  }  ?>
\ No newline at end of file | 
