Skip to content

Commit

Permalink
Added support to allow join tables all at once
Browse files Browse the repository at this point in the history
  • Loading branch information
qiang.xue committed Jan 29, 2009
1 parent bf224f2 commit 0e7adfc
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 20 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Version 1.0.2 to be released
- New: Added CModel::getValidatorsForAttribute (Qiang)
- New: Added CHtml::activeLabelEx and added 'required' option to CHtml::label (Qiang)
- New: Added array access support to CFormModel and CActiveRecord (Qiang)
- New: Added CActiveRecord::instantiate (Qiang)
- New: Added CActiveRecord::instantiate to support class table inheritance (Qiang)
- New: Added support to allow join tables all at once (Qiang)

Version 1.0.1 January 4, 2009
-----------------------------
Expand Down
19 changes: 18 additions & 1 deletion docs/guide/database.arr.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,31 @@ a hierarchy of related objects involving `N` `HAS_MANY` or `MANY_MANY`
relationships, it will take `N+1` SQL queries to obtain the needed results.
This means it needs to execute 3 SQL queries in the last example because of
the `posts` and `categories` properties. Other frameworks take a more
radical approach by using only one SQL query. At first look, this approach
radical approach by using only one SQL query. At first look, the radical approach
seems more efficient because fewer queries are being parsed and executed by
DBMS. It is in fact impractical in reality for two reasons. First, there
are many repetitive data columns in the result which takes extra time to
transmit and process. Second, the number of rows in the result set grows
exponentially with the number of tables involved, which makes it simply
unmanageable as more relationships are involved.

Since version 1.0.2, you can also enforce the relational query to be done with
only one SQL query. Simply append a [together|CActiveFinder::together] call
after [with()|CActiveRecord::with]. For example,

~~~
[php]
$posts=Post::model()->with(
'author.profile',
'author.posts',
'categories)->together()->findAll();
~~~

The above query will be done in one SQL query. Without calling [together|CActiveFinder::together],
this will need two SQL queries: one joins `Post`, `User` and `Profile` tables,
and the other joins `User` and `Post` tables.


Relational Query Options
------------------------

Expand Down
83 changes: 67 additions & 16 deletions framework/db/ar/CActiveFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@
*/
class CActiveFinder extends CComponent
{
/**
* @var boolean join all tables all at once. Defaults to false.
* This property is internally used.
* @since 1.0.2
*/
public $joinAll=false;
/**
* @var boolean whether the base model has limit or offset.
* This property is internally used.
* @since 1.0.2
*/
public $baseLimited;

private $_joinCount=0;
private $_joinTree;
private $_builder;
Expand All @@ -34,10 +47,24 @@ class CActiveFinder extends CComponent
public function __construct($model,$with)
{
$this->_builder=$model->getCommandBuilder();
$this->_joinTree=new CJoinElement($model);
$this->_joinTree=new CJoinElement($this,$model);
$this->buildJoinTree($this->_joinTree,$with);
}

/**
* Uses the most aggressive join approach.
* By default, several join statements may be generated in order to avoid
* fetching duplicated data. By calling this method, all tables will be joined
* together all at once.
* @return CActiveFinder the finder object
* @since 1.0.2
*/
public function together()
{
$this->joinAll=true;
return $this;
}

/**
* This is relational version of {@link CActiveRecord::find()}.
*/
Expand Down Expand Up @@ -151,13 +178,13 @@ private function buildJoinTree($parent,$with)
{
if(($pos=strrpos($with,'.'))!==false)
{
$parent=$this->buildJoinTree($this,substr($with,0,$pos));
$parent=$this->buildJoinTree($parent,substr($with,0,$pos));
$with=substr($with,$pos+1);
}
if(isset($parent->children[$with]))
return $parent->children[$with];
else if(($relation=$parent->model->getActiveRelation($with))!==null)
return new CJoinElement($relation,$parent,++$this->_joinCount);
return new CJoinElement($this,$relation,$parent,++$this->_joinCount);
else
throw new CDbException(Yii::t('yii','Relation "{name}" is not defined in active record class "{class}".',
array('{class}'=>get_class($parent->model), '{name}'=>$with)));
Expand All @@ -177,7 +204,7 @@ private function buildJoinTree($parent,$with)
$element->relation=$relation;
}
else if(is_string($value)) // the key is integer, so value is the relation name
return $this->buildJoinTree($parent,$value);
$this->buildJoinTree($parent,$value);
}
}
}
Expand All @@ -193,15 +220,6 @@ private function buildJoinTree($parent,$with)
*/
class CJoinElement
{
private $_builder;
private $_parent;
public $children=array();
public $tableAlias;
private $_pkAlias; // string or name=>alias
private $_columnAliases=array(); // name=>alias
private $_joined=false;
private $_table;

/**
* @var integer the unique ID of this tree node
*/
Expand All @@ -218,6 +236,23 @@ class CJoinElement
* @var array list of active records found by the queries. They are indexed by primary key values.
*/
public $records=array();
/**
* @var array list of child join elements
*/
public $children=array();
/**
* @var string table alias for this join element
*/
public $tableAlias;

private $_finder;
private $_builder;
private $_parent;
private $_pkAlias; // string or name=>alias
private $_columnAliases=array(); // name=>alias
private $_joined=false;
private $_table;
private $_related=array(); // PK, relation name, related PK => true

/**
* Constructor.
Expand All @@ -226,8 +261,9 @@ class CJoinElement
* @param CJoinElement the parent tree node
* @param integer the ID of this tree node that is unique among all the tree nodes
*/
public function __construct($relation,$parent=null,$id=0)
public function __construct($finder,$relation,$parent=null,$id=0)
{
$this->_finder=$finder;
$this->id=$id;
if($parent!==null)
{
Expand Down Expand Up @@ -268,6 +304,7 @@ public function find($criteria=null)
if($this->_parent===null) // root element
{
$query=new CJoinQuery($this,$criteria);
$this->_finder->baseLimited=($criteria->offset>=0 || $criteria->limit>=0);
$this->buildQuery($query);
$this->runQuery($query);
}
Expand Down Expand Up @@ -309,6 +346,7 @@ public function lazyFind($baseRecord)
{
$query->limit=$child->relation->limit;
$query->offset=$child->relation->offset;
$this->_finder->baseLimited=($query->offset>=0 || $query->limit>=0);
$query->groups[]=str_replace($child->relation->aliasToken.'.',$child->tableAlias.'.',$child->relation->group);
$query->havings[]=str_replace($child->relation->aliasToken.'.',$child->tableAlias.'.',$child->relation->having);
}
Expand Down Expand Up @@ -358,7 +396,8 @@ public function buildQuery($query)
{
foreach($this->children as $child)
{
if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation
|| ($this->_finder->joinAll && !$this->_finder->baseLimited))
{
$child->_joined=true;
$query->join($child);
Expand All @@ -382,6 +421,7 @@ public function runQuery($query)
* Populates the active records with the query data.
* @param CJoinQuery the query executed
* @param array a row of data
* @return CActiveRecord the populated record
*/
private function populateRecord($query,$row)
{
Expand Down Expand Up @@ -431,7 +471,18 @@ private function populateRecord($query,$row)
if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
$record->addRelatedRecord($child->relation->name,$childRecord,false);
else // has_many and many_many
$record->addRelatedRecord($child->relation->name,$childRecord,true);
{
// need to double check to avoid adding duplicated related objects
if($childRecord instanceof CActiveRecord)
$fpk=serialize($childRecord->getPrimaryKey());
else
$fpk=0;
if(!isset($this->_related[$pk][$child->relation->name][$fpk]))
{
$record->addRelatedRecord($child->relation->name,$childRecord,true);
$this->_related[$pk][$child->relation->name][$fpk]=true;
}
}
}

return $record;
Expand Down
2 changes: 1 addition & 1 deletion tests/ut/framework/db/ar/CActiveRecord2Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ public function testEagerRecursiveRelation()

public function testRelationWithCondition()
{
$posts=Post2::model()->with('comments')->findAllByPk(array(2,3,4));
$posts=Post2::model()->with('comments')->findAllByPk(array(2,3,4),array('order'=>'posts.id'));
$this->assertEquals(3,count($posts));
$this->assertEquals(2,count($posts[0]->comments));
$this->assertEquals(4,count($posts[1]->comments));
Expand Down
49 changes: 48 additions & 1 deletion tests/ut/framework/db/ar/CActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ public function testEagerRecursiveRelation()

public function testRelationWithCondition()
{
$posts=Post::model()->with('comments')->findAllByPk(array(2,3,4));
$posts=Post::model()->with('comments')->findAllByPk(array(2,3,4),array('order'=>'posts.id'));
$this->assertEquals(3,count($posts));
$this->assertEquals(2,count($posts[0]->comments));
$this->assertEquals(4,count($posts[1]->comments));
Expand Down Expand Up @@ -580,4 +580,51 @@ public function testRelationWithDynamicCondition()
$this->assertEquals($user->posts[1]->id,3);
$this->assertEquals($user->posts[2]->id,2);
}

public function testEagerTogetherRelation()
{
$post=Post::model()->with('author','firstComment','comments','categories')->findByPk(2);
$comments=$post->comments;
$this->assertEquals(array(
'id'=>2,
'username'=>'user2',
'password'=>'pass2',
'email'=>'email2'),$post->author->attributes);
$this->assertTrue($post->firstComment instanceof Comment);
$this->assertEquals(array(
'id'=>4,
'content'=>'comment 4',
'post_id'=>2,
'author_id'=>2),$post->firstComment->attributes);
$this->assertEquals(2,count($post->comments));
$this->assertEquals(array(
'id'=>5,
'content'=>'comment 5',
'post_id'=>2,
'author_id'=>2),$post->comments[0]->attributes);
$this->assertEquals(array(
'id'=>4,
'content'=>'comment 4',
'post_id'=>2,
'author_id'=>2),$post->comments[1]->attributes);
$this->assertEquals(2,count($post->categories));
$this->assertEquals(array(
'id'=>4,
'name'=>'cat 4',
'parent_id'=>1),$post->categories[0]->attributes);
$this->assertEquals(array(
'id'=>1,
'name'=>'cat 1',
'parent_id'=>null),$post->categories[1]->attributes);

$post=Post::model()->with('author','firstComment','comments','categories')->findByPk(4);
$this->assertEquals(array(
'id'=>2,
'username'=>'user2',
'password'=>'pass2',
'email'=>'email2'),$post->author->attributes);
$this->assertNull($post->firstComment);
$this->assertEquals(array(),$post->comments);
$this->assertEquals(array(),$post->categories);
}
}

0 comments on commit 0e7adfc

Please sign in to comment.