summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--HISTORY1
-rw-r--r--demos/quickstart/protected/pages/Database/ActiveRecord.page61
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php7
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php14
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php48
-rw-r--r--framework/Data/ActiveRecord/TActiveRecord.php152
6 files changed, 200 insertions, 83 deletions
diff --git a/HISTORY b/HISTORY
index f3e6881c..8f6507cc 100644
--- a/HISTORY
+++ b/HISTORY
@@ -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