summaryrefslogtreecommitdiff
path: root/framework/db/ar/CActiveFinder.php
diff options
context:
space:
mode:
Diffstat (limited to 'framework/db/ar/CActiveFinder.php')
-rw-r--r--framework/db/ar/CActiveFinder.php1647
1 files changed, 1647 insertions, 0 deletions
diff --git a/framework/db/ar/CActiveFinder.php b/framework/db/ar/CActiveFinder.php
new file mode 100644
index 0000000..2331100
--- /dev/null
+++ b/framework/db/ar/CActiveFinder.php
@@ -0,0 +1,1647 @@
+<?php
+/**
+ * CActiveRecord class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2011 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+/**
+ * CActiveFinder implements eager loading and lazy loading of related active records.
+ *
+ * When used in eager loading, this class provides the same set of find methods as
+ * {@link CActiveRecord}.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $
+ * @package system.db.ar
+ * @since 1.0
+ */
+class CActiveFinder extends CComponent
+{
+ /**
+ * @var boolean join all tables all at once. Defaults to false.
+ * This property is internally used.
+ */
+ public $joinAll=false;
+ /**
+ * @var boolean whether the base model has limit or offset.
+ * This property is internally used.
+ */
+ public $baseLimited=false;
+
+ private $_joinCount=0;
+ private $_joinTree;
+ private $_builder;
+
+ /**
+ * Constructor.
+ * A join tree is built up based on the declared relationships between active record classes.
+ * @param CActiveRecord $model the model that initiates the active finding process
+ * @param mixed $with the relation names to be actively looked for
+ */
+ public function __construct($model,$with)
+ {
+ $this->_builder=$model->getCommandBuilder();
+ $this->_joinTree=new CJoinElement($this,$model);
+ $this->buildJoinTree($this->_joinTree,$with);
+ }
+
+ /**
+ * Do not call this method. This method is used internally to perform the relational query
+ * based on the given DB criteria.
+ * @param CDbCriteria $criteria the DB criteria
+ * @param boolean $all whether to bring back all records
+ * @return mixed the query result
+ */
+ public function query($criteria,$all=false)
+ {
+ $this->joinAll=$criteria->together===true;
+ $this->_joinTree->beforeFind(false);
+
+ if($criteria->alias!='')
+ {
+ $this->_joinTree->tableAlias=$criteria->alias;
+ $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($criteria->alias);
+ }
+
+ $this->_joinTree->find($criteria);
+ $this->_joinTree->afterFind();
+
+ if($all)
+ {
+ $result = array_values($this->_joinTree->records);
+ if ($criteria->index!==null)
+ {
+ $index=$criteria->index;
+ $array=array();
+ foreach($result as $object)
+ $array[$object->$index]=$object;
+ $result=$array;
+ }
+ }
+ else if(count($this->_joinTree->records))
+ $result = reset($this->_joinTree->records);
+ else
+ $result = null;
+
+ $this->destroyJoinTree();
+ return $result;
+ }
+
+ /**
+ * This method is internally called.
+ * @param string $sql the SQL statement
+ * @param array $params parameters to be bound to the SQL statement
+ * @return CActiveRecord
+ */
+ public function findBySql($sql,$params=array())
+ {
+ Yii::trace(get_class($this->_joinTree->model).'.findBySql() eagerly','system.db.ar.CActiveRecord');
+ if(($row=$this->_builder->createSqlCommand($sql,$params)->queryRow())!==false)
+ {
+ $baseRecord=$this->_joinTree->model->populateRecord($row,false);
+ $this->_joinTree->beforeFind(false);
+ $this->_joinTree->findWithBase($baseRecord);
+ $this->_joinTree->afterFind();
+ $this->destroyJoinTree();
+ return $baseRecord;
+ }
+ else
+ $this->destroyJoinTree();
+ }
+
+ /**
+ * This method is internally called.
+ * @param string $sql the SQL statement
+ * @param array $params parameters to be bound to the SQL statement
+ * @return CActiveRecord[]
+ */
+ public function findAllBySql($sql,$params=array())
+ {
+ Yii::trace(get_class($this->_joinTree->model).'.findAllBySql() eagerly','system.db.ar.CActiveRecord');
+ if(($rows=$this->_builder->createSqlCommand($sql,$params)->queryAll())!==array())
+ {
+ $baseRecords=$this->_joinTree->model->populateRecords($rows,false);
+ $this->_joinTree->beforeFind(false);
+ $this->_joinTree->findWithBase($baseRecords);
+ $this->_joinTree->afterFind();
+ $this->destroyJoinTree();
+ return $baseRecords;
+ }
+ else
+ {
+ $this->destroyJoinTree();
+ return array();
+ }
+ }
+
+ /**
+ * This method is internally called.
+ * @param CDbCriteria $criteria the query criteria
+ * @return string
+ */
+ public function count($criteria)
+ {
+ Yii::trace(get_class($this->_joinTree->model).'.count() eagerly','system.db.ar.CActiveRecord');
+ $this->joinAll=$criteria->together!==true;
+
+ $alias=$criteria->alias===null ? 't' : $criteria->alias;
+ $this->_joinTree->tableAlias=$alias;
+ $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($alias);
+
+ $n=$this->_joinTree->count($criteria);
+ $this->destroyJoinTree();
+ return $n;
+ }
+
+ /**
+ * Finds the related objects for the specified active record.
+ * This method is internally invoked by {@link CActiveRecord} to support lazy loading.
+ * @param CActiveRecord $baseRecord the base record whose related objects are to be loaded
+ */
+ public function lazyFind($baseRecord)
+ {
+ $this->_joinTree->lazyFind($baseRecord);
+ if(!empty($this->_joinTree->children))
+ {
+ $child=reset($this->_joinTree->children);
+ $child->afterFind();
+ }
+ $this->destroyJoinTree();
+ }
+
+ private function destroyJoinTree()
+ {
+ if($this->_joinTree!==null)
+ $this->_joinTree->destroy();
+ $this->_joinTree=null;
+ }
+
+ /**
+ * Builds up the join tree representing the relationships involved in this query.
+ * @param CJoinElement $parent the parent tree node
+ * @param mixed $with the names of the related objects relative to the parent tree node
+ * @param array $options additional query options to be merged with the relation
+ */
+ private function buildJoinTree($parent,$with,$options=null)
+ {
+ if($parent instanceof CStatElement)
+ throw new CDbException(Yii::t('yii','The STAT relation "{name}" cannot have child relations.',
+ array('{name}'=>$parent->relation->name)));
+
+ if(is_string($with))
+ {
+ if(($pos=strrpos($with,'.'))!==false)
+ {
+ $parent=$this->buildJoinTree($parent,substr($with,0,$pos));
+ $with=substr($with,$pos+1);
+ }
+
+ // named scope
+ $scopes=array();
+ if(($pos=strpos($with,':'))!==false)
+ {
+ $scopes=explode(':',substr($with,$pos+1));
+ $with=substr($with,0,$pos);
+ }
+
+ if(isset($parent->children[$with]) && $parent->children[$with]->master===null)
+ return $parent->children[$with];
+
+ if(($relation=$parent->model->getActiveRelation($with))===null)
+ throw new CDbException(Yii::t('yii','Relation "{name}" is not defined in active record class "{class}".',
+ array('{class}'=>get_class($parent->model), '{name}'=>$with)));
+
+ $relation=clone $relation;
+ $model=CActiveRecord::model($relation->className);
+ if($relation instanceof CActiveRelation)
+ {
+ $oldAlias=$model->getTableAlias(false,false);
+ if(isset($options['alias']))
+ $model->setTableAlias($options['alias']);
+ else if($relation->alias===null)
+ $model->setTableAlias($relation->name);
+ else
+ $model->setTableAlias($relation->alias);
+ }
+
+ if(($scope=$model->defaultScope())!==array())
+ $relation->mergeWith($scope,true);
+
+ if(!empty($relation->scopes))
+ $scopes=array_merge($scopes,(array)$relation->scopes); // no need for complex merging
+
+ if(!empty($options['scopes']))
+ $scopes=array_merge($scopes,(array)$options['scopes']); // no need for complex merging
+
+ if($scopes!==array())
+ {
+ $scs=$model->scopes();
+ foreach($scopes as $k=>$v)
+ {
+ if(is_integer($k))
+ {
+ if(is_string($v))
+ {
+ if(isset($scs[$v]))
+ {
+ $relation->mergeWith($scs[$v],true);
+ continue;
+ }
+ $scope=$v;
+ $params=array();
+ }
+ else if(is_array($v))
+ {
+ $scope=key($v);
+ $params=current($v);
+ }
+ }
+ else if(is_string($k))
+ {
+ $scope=$k;
+ $params=$v;
+ }
+
+ $model->resetScope();
+ call_user_func_array(array($model,$scope),(array)$params);
+ $relation->mergeWith($model->getDbCriteria(),true);
+ }
+ }
+
+ // dynamic options
+ if($options!==null)
+ $relation->mergeWith($options);
+
+ if($relation instanceof CActiveRelation)
+ $model->setTableAlias($oldAlias);
+
+ if($relation instanceof CStatRelation)
+ return new CStatElement($this,$relation,$parent);
+ else
+ {
+ if(isset($parent->children[$with]))
+ {
+ $element=$parent->children[$with];
+ $element->relation=$relation;
+ }
+ else
+ $element=new CJoinElement($this,$relation,$parent,++$this->_joinCount);
+ if(!empty($relation->through))
+ {
+ $slave=$this->buildJoinTree($parent,$relation->through,array('select'=>false));
+ $slave->master=$element;
+ $element->slave=$slave;
+ }
+ $parent->children[$with]=$element;
+ if(!empty($relation->with))
+ $this->buildJoinTree($element,$relation->with);
+ return $element;
+ }
+ }
+
+ // $with is an array, keys are relation name, values are relation spec
+ foreach($with as $key=>$value)
+ {
+ if(is_string($value)) // the value is a relation name
+ $this->buildJoinTree($parent,$value);
+ else if(is_string($key) && is_array($value))
+ $this->buildJoinTree($parent,$key,$value);
+ }
+ }
+}
+
+
+/**
+ * CJoinElement represents a tree node in the join tree created by {@link CActiveFinder}.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $
+ * @package system.db.ar
+ * @since 1.0
+ */
+class CJoinElement
+{
+ /**
+ * @var integer the unique ID of this tree node
+ */
+ public $id;
+ /**
+ * @var CActiveRelation the relation represented by this tree node
+ */
+ public $relation;
+ /**
+ * @var CActiveRelation the master relation
+ */
+ public $master;
+ /**
+ * @var CActiveRelation the slave relation
+ */
+ public $slave;
+ /**
+ * @var CActiveRecord the model associated with this tree node
+ */
+ public $model;
+ /**
+ * @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 array list of stat elements
+ */
+ public $stats=array();
+ /**
+ * @var string table alias for this join element
+ */
+ public $tableAlias;
+ /**
+ * @var string the quoted table alias for this element
+ */
+ public $rawTableAlias;
+
+ 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.
+ * @param CActiveFinder $finder the finder
+ * @param mixed $relation the relation (if the third parameter is not null)
+ * or the model (if the third parameter is null) associated with this tree node.
+ * @param CJoinElement $parent the parent tree node
+ * @param integer $id the ID of this tree node that is unique among all the tree nodes
+ */
+ public function __construct($finder,$relation,$parent=null,$id=0)
+ {
+ $this->_finder=$finder;
+ $this->id=$id;
+ if($parent!==null)
+ {
+ $this->relation=$relation;
+ $this->_parent=$parent;
+ $this->model=CActiveRecord::model($relation->className);
+ $this->_builder=$this->model->getCommandBuilder();
+ $this->tableAlias=$relation->alias===null?$relation->name:$relation->alias;
+ $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias);
+ $this->_table=$this->model->getTableSchema();
+ }
+ else // root element, the first parameter is the model.
+ {
+ $this->model=$relation;
+ $this->_builder=$relation->getCommandBuilder();
+ $this->_table=$relation->getTableSchema();
+ $this->tableAlias=$this->model->getTableAlias();
+ $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias);
+ }
+
+ // set up column aliases, such as t1_c2
+ $table=$this->_table;
+ if($this->model->getDbConnection()->getDriverName()==='oci') // Issue 482
+ $prefix='T'.$id.'_C';
+ else
+ $prefix='t'.$id.'_c';
+ foreach($table->getColumnNames() as $key=>$name)
+ {
+ $alias=$prefix.$key;
+ $this->_columnAliases[$name]=$alias;
+ if($table->primaryKey===$name)
+ $this->_pkAlias=$alias;
+ else if(is_array($table->primaryKey) && in_array($name,$table->primaryKey))
+ $this->_pkAlias[$name]=$alias;
+ }
+ }
+
+ /**
+ * Removes references to child elements and finder to avoid circular references.
+ * This is internally used.
+ */
+ public function destroy()
+ {
+ if(!empty($this->children))
+ {
+ foreach($this->children as $child)
+ $child->destroy();
+ }
+ unset($this->_finder, $this->_parent, $this->model, $this->relation, $this->master, $this->slave, $this->records, $this->children, $this->stats);
+ }
+
+ /**
+ * Performs the recursive finding with the criteria.
+ * @param CDbCriteria $criteria the query criteria
+ */
+ 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->_finder->baseLimited=false;
+ $this->runQuery($query);
+ }
+ else if(!$this->_joined && !empty($this->_parent->records)) // not joined before
+ {
+ $query=new CJoinQuery($this->_parent);
+ $this->_joined=true;
+ $query->join($this);
+ $this->buildQuery($query);
+ $this->_parent->runQuery($query);
+ }
+
+ foreach($this->children as $child) // find recursively
+ $child->find();
+
+ foreach($this->stats as $stat)
+ $stat->query();
+ }
+
+ /**
+ * Performs lazy find with the specified base record.
+ * @param CActiveRecord $baseRecord the active record whose related object is to be fetched.
+ */
+ public function lazyFind($baseRecord)
+ {
+ if(is_string($this->_table->primaryKey))
+ $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord;
+ else
+ {
+ $pk=array();
+ foreach($this->_table->primaryKey as $name)
+ $pk[$name]=$baseRecord->$name;
+ $this->records[serialize($pk)]=$baseRecord;
+ }
+
+ foreach($this->stats as $stat)
+ $stat->query();
+
+ switch(count($this->children))
+ {
+ case 0:
+ return;
+ break;
+ case 1:
+ $child=reset($this->children);
+ break;
+ default: // bridge(s) inside
+ $child=end($this->children);
+ break;
+ }
+
+ $query=new CJoinQuery($child);
+ $query->selects=array();
+ $query->selects[]=$child->getColumnSelect($child->relation->select);
+ $query->conditions=array();
+ $query->conditions[]=$child->relation->condition;
+ $query->conditions[]=$child->relation->on;
+ $query->groups[]=$child->relation->group;
+ $query->joins[]=$child->relation->join;
+ $query->havings[]=$child->relation->having;
+ $query->orders[]=$child->relation->order;
+ if(is_array($child->relation->params))
+ $query->params=$child->relation->params;
+ $query->elements[$child->id]=true;
+ if($child->relation instanceof CHasManyRelation)
+ {
+ $query->limit=$child->relation->limit;
+ $query->offset=$child->relation->offset;
+ }
+
+ $child->beforeFind();
+ $child->applyLazyCondition($query,$baseRecord);
+
+ $this->_joined=true;
+ $child->_joined=true;
+
+ $this->_finder->baseLimited=false;
+ $child->buildQuery($query);
+ $child->runQuery($query);
+ foreach($child->children as $c)
+ $c->find();
+
+ if(empty($child->records))
+ return;
+ if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
+ $baseRecord->addRelatedRecord($child->relation->name,reset($child->records),false);
+ else // has_many and many_many
+ {
+ foreach($child->records as $record)
+ {
+ if($child->relation->index!==null)
+ $index=$record->{$child->relation->index};
+ else
+ $index=true;
+ $baseRecord->addRelatedRecord($child->relation->name,$record,$index);
+ }
+ }
+ }
+
+ /**
+ * Apply Lazy Condition
+ * @param CJoinQuery $query represents a JOIN SQL statements
+ * @param CActiveRecord $record the active record whose related object is to be fetched.
+ */
+ private function applyLazyCondition($query,$record)
+ {
+ $schema=$this->_builder->getSchema();
+ $parent=$this->_parent;
+ if($this->relation instanceof CManyManyRelation)
+ {
+ if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".',
+ array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name)));
+
+ if(($joinTable=$schema->getTable($matches[1]))===null)
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1])));
+ $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);
+
+
+ $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias);
+ $parentCondition=array();
+ $childCondition=array();
+ $count=0;
+ $params=array();
+
+ $fkDefined=true;
+ foreach($fks as $i=>$fk)
+ {
+ if(isset($joinTable->foreignKeys[$fk])) // FK defined
+ {
+ list($tableName,$pk)=$joinTable->foreignKeys[$fk];
+ if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName))
+ {
+ $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count;
+ $params[':ypl'.$count]=$record->$pk;
+ $count++;
+ }
+ else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName))
+ $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+
+ if(!$fkDefined)
+ {
+ $parentCondition=array();
+ $childCondition=array();
+ $count=0;
+ $params=array();
+ foreach($fks as $i=>$fk)
+ {
+ if($i<count($parent->_table->primaryKey))
+ {
+ $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
+ $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count;
+ $params[':ypl'.$count]=$record->$pk;
+ $count++;
+ }
+ else
+ {
+ $j=$i-count($parent->_table->primaryKey);
+ $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
+ $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ }
+ }
+ }
+
+ if($parentCondition!==array() && $childCondition!==array())
+ {
+ $join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON ';
+ $join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')';
+ if(!empty($this->relation->on))
+ $join.=' AND ('.$this->relation->on.')';
+ $query->joins[]=$join;
+ foreach($params as $name=>$value)
+ $query->params[$name]=$value;
+ }
+ else
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));
+ }
+ else
+ {
+ $element=$this;
+ while($element->slave!==null)
+ {
+ $query->joins[]=$element->slave->joinOneMany($element->slave,$element->relation->foreignKey,$element,$parent);
+ $element=$element->slave;
+ }
+ $fks=is_array($element->relation->foreignKey) ? $element->relation->foreignKey : preg_split('/\s*,\s*/',$element->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
+ $prefix=$element->getColumnPrefix();
+ $params=array();
+ foreach($fks as $i=>$fk)
+ {
+ if(!is_int($i))
+ {
+ $pk=$fk;
+ $fk=$i;
+ }
+
+ if($this->relation instanceof CBelongsToRelation)
+ {
+ if(is_int($i))
+ {
+ if(isset($parent->_table->foreignKeys[$fk])) // FK defined
+ $pk=$parent->_table->foreignKeys[$fk][1];
+ else if(is_array($this->_table->primaryKey)) // composite PK
+ $pk=$this->_table->primaryKey[$i];
+ else
+ $pk=$this->_table->primaryKey;
+ }
+ $params[$pk]=$record->$fk;
+ }
+ else
+ {
+ if(is_int($i))
+ {
+ if(isset($this->_table->foreignKeys[$fk])) // FK defined
+ $pk=$this->_table->foreignKeys[$fk][1];
+ else if(is_array($parent->_table->primaryKey)) // composite PK
+ $pk=$parent->_table->primaryKey[$i];
+ else
+ $pk=$parent->_table->primaryKey;
+ }
+ $params[$fk]=$record->$pk;
+ }
+ }
+ $count=0;
+ foreach($params as $name=>$value)
+ {
+ $query->conditions[]=$prefix.$schema->quoteColumnName($name).'=:ypl'.$count;
+ $query->params[':ypl'.$count]=$value;
+ $count++;
+ }
+ }
+ }
+
+ /**
+ * Performs the eager loading with the base records ready.
+ * @param mixed $baseRecords the available base record(s).
+ */
+ public function findWithBase($baseRecords)
+ {
+ if(!is_array($baseRecords))
+ $baseRecords=array($baseRecords);
+ if(is_string($this->_table->primaryKey))
+ {
+ foreach($baseRecords as $baseRecord)
+ $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord;
+ }
+ else
+ {
+ foreach($baseRecords as $baseRecord)
+ {
+ $pk=array();
+ foreach($this->_table->primaryKey as $name)
+ $pk[$name]=$baseRecord->$name;
+ $this->records[serialize($pk)]=$baseRecord;
+ }
+ }
+
+ $query=new CJoinQuery($this);
+ $this->buildQuery($query);
+ if(count($query->joins)>1)
+ $this->runQuery($query);
+ foreach($this->children as $child)
+ $child->find();
+
+ foreach($this->stats as $stat)
+ $stat->query();
+ }
+
+ /**
+ * Count the number of primary records returned by the join statement.
+ * @param CDbCriteria $criteria the query criteria
+ * @return string number of primary records. Note: type is string to keep max. precision.
+ */
+ public function count($criteria=null)
+ {
+ $query=new CJoinQuery($this,$criteria);
+ // ensure only one big join statement is used
+ $this->_finder->baseLimited=false;
+ $this->_finder->joinAll=true;
+ $this->buildQuery($query);
+
+ $select=is_array($criteria->select) ? implode(',',$criteria->select) : $criteria->select;
+ if($select!=='*' && !strncasecmp($select,'count',5))
+ $query->selects=array($select);
+ else if(is_string($this->_table->primaryKey))
+ {
+ $prefix=$this->getColumnPrefix();
+ $schema=$this->_builder->getSchema();
+ $column=$prefix.$schema->quoteColumnName($this->_table->primaryKey);
+ $query->selects=array("COUNT(DISTINCT $column)");
+ }
+ else
+ $query->selects=array("COUNT(*)");
+
+ $query->orders=$query->groups=$query->havings=array();
+ $query->limit=$query->offset=-1;
+ $command=$query->createCommand($this->_builder);
+ return $command->queryScalar();
+ }
+
+ /**
+ * Calls {@link CActiveRecord::beforeFind}.
+ * @param boolean $isChild whether is called for a child
+ */
+ public function beforeFind($isChild=true)
+ {
+ if($isChild)
+ $this->model->beforeFindInternal();
+
+ foreach($this->children as $child)
+ $child->beforeFind(true);
+ }
+
+ /**
+ * Calls {@link CActiveRecord::afterFind} of all the records.
+ */
+ public function afterFind()
+ {
+ foreach($this->records as $record)
+ $record->afterFindInternal();
+ foreach($this->children as $child)
+ $child->afterFind();
+
+ $this->children = null;
+ }
+
+ /**
+ * Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes.
+ * @param CJoinQuery $query the query being built up
+ */
+ public function buildQuery($query)
+ {
+ foreach($this->children as $child)
+ {
+ if($child->master!==null)
+ $child->_joined=true;
+ else if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation
+ || $this->_finder->joinAll || $child->relation->together || (!$this->_finder->baseLimited && $child->relation->together===null))
+ {
+ $child->_joined=true;
+ $query->join($child);
+ $child->buildQuery($query);
+ }
+ }
+ }
+
+ /**
+ * Executes the join query and populates the query results.
+ * @param CJoinQuery $query the query to be executed.
+ */
+ public function runQuery($query)
+ {
+ $command=$query->createCommand($this->_builder);
+ foreach($command->queryAll() as $row)
+ $this->populateRecord($query,$row);
+ }
+
+ /**
+ * Populates the active records with the query data.
+ * @param CJoinQuery $query the query executed
+ * @param array $row a row of data
+ * @return CActiveRecord the populated record
+ */
+ private function populateRecord($query,$row)
+ {
+ // determine the primary key value
+ if(is_string($this->_pkAlias)) // single key
+ {
+ if(isset($row[$this->_pkAlias]))
+ $pk=$row[$this->_pkAlias];
+ else // no matching related objects
+ return null;
+ }
+ else // is_array, composite key
+ {
+ $pk=array();
+ foreach($this->_pkAlias as $name=>$alias)
+ {
+ if(isset($row[$alias]))
+ $pk[$name]=$row[$alias];
+ else // no matching related objects
+ return null;
+ }
+ $pk=serialize($pk);
+ }
+
+ // retrieve or populate the record according to the primary key value
+ if(isset($this->records[$pk]))
+ $record=$this->records[$pk];
+ else
+ {
+ $attributes=array();
+ $aliases=array_flip($this->_columnAliases);
+ foreach($row as $alias=>$value)
+ {
+ if(isset($aliases[$alias]))
+ $attributes[$aliases[$alias]]=$value;
+ }
+ $record=$this->model->populateRecord($attributes,false);
+ foreach($this->children as $child)
+ {
+ if(!empty($child->relation->select))
+ $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation);
+ }
+ $this->records[$pk]=$record;
+ }
+
+ // populate child records recursively
+ foreach($this->children as $child)
+ {
+ if(!isset($query->elements[$child->id]) || empty($child->relation->select))
+ continue;
+ $childRecord=$child->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
+ {
+ // 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]))
+ {
+ if($childRecord instanceof CActiveRecord && $child->relation->index!==null)
+ $index=$childRecord->{$child->relation->index};
+ else
+ $index=true;
+ $record->addRelatedRecord($child->relation->name,$childRecord,$index);
+ $this->_related[$pk][$child->relation->name][$fpk]=true;
+ }
+ }
+ }
+
+ return $record;
+ }
+
+ /**
+ * @return string the table name and the table alias (if any). This can be used directly in SQL query without escaping.
+ */
+ public function getTableNameWithAlias()
+ {
+ if($this->tableAlias!==null)
+ return $this->_table->rawName . ' ' . $this->rawTableAlias;
+ else
+ return $this->_table->rawName;
+ }
+
+ /**
+ * Generates the list of columns to be selected.
+ * Columns will be properly aliased and primary keys will be added to selection if they are not specified.
+ * @param mixed $select columns to be selected. Defaults to '*', indicating all columns.
+ * @return string the column selection
+ */
+ public function getColumnSelect($select='*')
+ {
+ $schema=$this->_builder->getSchema();
+ $prefix=$this->getColumnPrefix();
+ $columns=array();
+ if($select==='*')
+ {
+ foreach($this->_table->getColumnNames() as $name)
+ $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_columnAliases[$name]);
+ }
+ else
+ {
+ if(is_string($select))
+ $select=explode(',',$select);
+ $selected=array();
+ foreach($select as $name)
+ {
+ $name=trim($name);
+ $matches=array();
+ if(($pos=strrpos($name,'.'))!==false)
+ $key=substr($name,$pos+1);
+ else
+ $key=$name;
+ $key=trim($key,'\'"`');
+
+ if($key==='*')
+ {
+ foreach($this->_table->columns as $name=>$column)
+ {
+ $alias=$this->_columnAliases[$name];
+ if(!isset($selected[$alias]))
+ {
+ $columns[]=$prefix.$column->rawName.' AS '.$schema->quoteColumnName($alias);
+ $selected[$alias]=1;
+ }
+ }
+ continue;
+ }
+
+ if(isset($this->_columnAliases[$key])) // simple column names
+ {
+ $columns[]=$prefix.$schema->quoteColumnName($key).' AS '.$schema->quoteColumnName($this->_columnAliases[$key]);
+ $selected[$this->_columnAliases[$key]]=1;
+ }
+ else if(preg_match('/^(.*?)\s+AS\s+(\w+)$/im',$name,$matches)) // if the column is already aliased
+ {
+ $alias=$matches[2];
+ if(!isset($this->_columnAliases[$alias]) || $this->_columnAliases[$alias]!==$alias)
+ {
+ $this->_columnAliases[$alias]=$alias;
+ $columns[]=$name;
+ $selected[$alias]=1;
+ }
+ }
+ else
+ throw new CDbException(Yii::t('yii','Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.',
+ array('{class}'=>get_class($this->model), '{column}'=>$name)));
+ }
+ // add primary key selection if they are not selected
+ if(is_string($this->_pkAlias) && !isset($selected[$this->_pkAlias]))
+ $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias);
+ else if(is_array($this->_pkAlias))
+ {
+ foreach($this->_table->primaryKey as $name)
+ if(!isset($selected[$name]))
+ $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_pkAlias[$name]);
+ }
+ }
+
+ return implode(', ',$columns);
+ }
+
+ /**
+ * @return string the primary key selection
+ */
+ public function getPrimaryKeySelect()
+ {
+ $schema=$this->_builder->getSchema();
+ $prefix=$this->getColumnPrefix();
+ $columns=array();
+ if(is_string($this->_pkAlias))
+ $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias);
+ else if(is_array($this->_pkAlias))
+ {
+ foreach($this->_pkAlias as $name=>$alias)
+ $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($alias);
+ }
+ return implode(', ',$columns);
+ }
+
+ /**
+ * @return string the condition that specifies only the rows with the selected primary key values.
+ */
+ public function getPrimaryKeyRange()
+ {
+ if(empty($this->records))
+ return '';
+ $values=array_keys($this->records);
+ if(is_array($this->_table->primaryKey))
+ {
+ foreach($values as &$value)
+ $value=unserialize($value);
+ }
+ return $this->_builder->createInCondition($this->_table,$this->_table->primaryKey,$values,$this->getColumnPrefix());
+ }
+
+ /**
+ * @return string the column prefix for column reference disambiguation
+ */
+ public function getColumnPrefix()
+ {
+ if($this->tableAlias!==null)
+ return $this->rawTableAlias.'.';
+ else
+ return $this->_table->rawName.'.';
+ }
+
+ /**
+ * @return string the join statement (this node joins with its parent)
+ */
+ public function getJoinCondition()
+ {
+ $parent=$this->_parent;
+ if($this->relation instanceof CManyManyRelation)
+ {
+ if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".',
+ array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name)));
+
+ $schema=$this->_builder->getSchema();
+ if(($joinTable=$schema->getTable($matches[1]))===null)
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1])));
+ $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);
+
+ return $this->joinManyMany($joinTable,$fks,$parent);
+ }
+ else
+ {
+ $fks=is_array($this->relation->foreignKey) ? $this->relation->foreignKey : preg_split('/\s*,\s*/',$this->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
+ if($this->relation instanceof CBelongsToRelation)
+ {
+ $pke=$this;
+ $fke=$parent;
+ }
+ else if($this->slave===null)
+ {
+ $pke=$parent;
+ $fke=$this;
+ }
+ else
+ {
+ $pke=$this;
+ $fke=$this->slave;
+ }
+ return $this->joinOneMany($fke,$fks,$pke,$parent);
+ }
+ }
+
+ /**
+ * Generates the join statement for one-many relationship.
+ * This works for HAS_ONE, HAS_MANY and BELONGS_TO.
+ * @param CJoinElement $fke the join element containing foreign keys
+ * @param array $fks the foreign keys
+ * @param CJoinElement $pke the join element containg primary keys
+ * @param CJoinElement $parent the parent join element
+ * @return string the join statement
+ * @throws CDbException if a foreign key is invalid
+ */
+ private function joinOneMany($fke,$fks,$pke,$parent)
+ {
+ $schema=$this->_builder->getSchema();
+ $joins=array();
+ if(is_string($fks))
+ $fks=preg_split('/\s*,\s*/',$fks,-1,PREG_SPLIT_NO_EMPTY);
+ foreach($fks as $i=>$fk)
+ {
+ if(!is_int($i))
+ {
+ $pk=$fk;
+ $fk=$i;
+ }
+
+ if(!isset($fke->_table->columns[$fk]))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$fke->_table->name)));
+
+ if(is_int($i))
+ {
+ if(isset($fke->_table->foreignKeys[$fk]) && $schema->compareTableNames($pke->_table->rawName, $fke->_table->foreignKeys[$fk][0]))
+ $pk=$fke->_table->foreignKeys[$fk][1];
+ else // FK constraints undefined
+ {
+ if(is_array($pke->_table->primaryKey)) // composite PK
+ $pk=$pke->_table->primaryKey[$i];
+ else
+ $pk=$pke->_table->primaryKey;
+ }
+ }
+
+ $joins[]=$fke->getColumnPrefix().$schema->quoteColumnName($fk) . '=' . $pke->getColumnPrefix().$schema->quoteColumnName($pk);
+ }
+ if(!empty($this->relation->on))
+ $joins[]=$this->relation->on;
+ return $this->relation->joinType . ' ' . $this->getTableNameWithAlias() . ' ON (' . implode(') AND (',$joins).')';
+ }
+
+ /**
+ * Generates the join statement for many-many relationship.
+ * @param CDbTableSchema $joinTable the join table
+ * @param array $fks the foreign keys
+ * @param CJoinElement $parent the parent join element
+ * @return string the join statement
+ * @throws CDbException if a foreign key is invalid
+ */
+ private function joinManyMany($joinTable,$fks,$parent)
+ {
+ $schema=$this->_builder->getSchema();
+ $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias);
+ $parentCondition=array();
+ $childCondition=array();
+
+ $fkDefined=true;
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($joinTable->columns[$fk]))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name)));
+
+ if(isset($joinTable->foreignKeys[$fk]))
+ {
+ list($tableName,$pk)=$joinTable->foreignKeys[$fk];
+ if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName))
+ $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName))
+ $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+
+ if(!$fkDefined)
+ {
+ $parentCondition=array();
+ $childCondition=array();
+ foreach($fks as $i=>$fk)
+ {
+ if($i<count($parent->_table->primaryKey))
+ {
+ $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
+ $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ }
+ else
+ {
+ $j=$i-count($parent->_table->primaryKey);
+ $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
+ $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
+ }
+ }
+ }
+
+ if($parentCondition!==array() && $childCondition!==array())
+ {
+ $join=$this->relation->joinType.' '.$joinTable->rawName.' '.$joinAlias;
+ $join.=' ON ('.implode(') AND (',$parentCondition).')';
+ $join.=' '.$this->relation->joinType.' '.$this->getTableNameWithAlias();
+ $join.=' ON ('.implode(') AND (',$childCondition).')';
+ if(!empty($this->relation->on))
+ $join.=' AND ('.$this->relation->on.')';
+ return $join;
+ }
+ else
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));
+ }
+}
+
+
+/**
+ * CJoinQuery represents a JOIN SQL statement.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $
+ * @package system.db.ar
+ * @since 1.0
+ */
+class CJoinQuery
+{
+ /**
+ * @var array list of column selections
+ */
+ public $selects=array();
+ /**
+ * @var boolean whether to select distinct result set
+ */
+ public $distinct=false;
+ /**
+ * @var array list of join statement
+ */
+ public $joins=array();
+ /**
+ * @var array list of WHERE clauses
+ */
+ public $conditions=array();
+ /**
+ * @var array list of ORDER BY clauses
+ */
+ public $orders=array();
+ /**
+ * @var array list of GROUP BY clauses
+ */
+ public $groups=array();
+ /**
+ * @var array list of HAVING clauses
+ */
+ public $havings=array();
+ /**
+ * @var integer row limit
+ */
+ public $limit=-1;
+ /**
+ * @var integer row offset
+ */
+ public $offset=-1;
+ /**
+ * @var array list of query parameters
+ */
+ public $params=array();
+ /**
+ * @var array list of join element IDs (id=>true)
+ */
+ public $elements=array();
+
+ /**
+ * Constructor.
+ * @param CJoinElement $joinElement The root join tree.
+ * @param CDbCriteria $criteria the query criteria
+ */
+ public function __construct($joinElement,$criteria=null)
+ {
+ if($criteria!==null)
+ {
+ $this->selects[]=$joinElement->getColumnSelect($criteria->select);
+ $this->joins[]=$joinElement->getTableNameWithAlias();
+ $this->joins[]=$criteria->join;
+ $this->conditions[]=$criteria->condition;
+ $this->orders[]=$criteria->order;
+ $this->groups[]=$criteria->group;
+ $this->havings[]=$criteria->having;
+ $this->limit=$criteria->limit;
+ $this->offset=$criteria->offset;
+ $this->params=$criteria->params;
+ if(!$this->distinct && $criteria->distinct)
+ $this->distinct=true;
+ }
+ else
+ {
+ $this->selects[]=$joinElement->getPrimaryKeySelect();
+ $this->joins[]=$joinElement->getTableNameWithAlias();
+ $this->conditions[]=$joinElement->getPrimaryKeyRange();
+ }
+ $this->elements[$joinElement->id]=true;
+ }
+
+ /**
+ * Joins with another join element
+ * @param CJoinElement $element the element to be joined
+ */
+ public function join($element)
+ {
+ if($element->slave!==null)
+ $this->join($element->slave);
+ if(!empty($element->relation->select))
+ $this->selects[]=$element->getColumnSelect($element->relation->select);
+ $this->conditions[]=$element->relation->condition;
+ $this->orders[]=$element->relation->order;
+ $this->joins[]=$element->getJoinCondition();
+ $this->joins[]=$element->relation->join;
+ $this->groups[]=$element->relation->group;
+ $this->havings[]=$element->relation->having;
+
+ if(is_array($element->relation->params))
+ {
+ if(is_array($this->params))
+ $this->params=array_merge($this->params,$element->relation->params);
+ else
+ $this->params=$element->relation->params;
+ }
+ $this->elements[$element->id]=true;
+ }
+
+ /**
+ * Creates the SQL statement.
+ * @param CDbCommandBuilder $builder the command builder
+ * @return string the SQL statement
+ */
+ public function createCommand($builder)
+ {
+ $sql=($this->distinct ? 'SELECT DISTINCT ':'SELECT ') . implode(', ',$this->selects);
+ $sql.=' FROM ' . implode(' ',$this->joins);
+
+ $conditions=array();
+ foreach($this->conditions as $condition)
+ if($condition!=='')
+ $conditions[]=$condition;
+ if($conditions!==array())
+ $sql.=' WHERE (' . implode(') AND (',$conditions).')';
+
+ $groups=array();
+ foreach($this->groups as $group)
+ if($group!=='')
+ $groups[]=$group;
+ if($groups!==array())
+ $sql.=' GROUP BY ' . implode(', ',$groups);
+
+ $havings=array();
+ foreach($this->havings as $having)
+ if($having!=='')
+ $havings[]=$having;
+ if($havings!==array())
+ $sql.=' HAVING (' . implode(') AND (',$havings).')';
+
+ $orders=array();
+ foreach($this->orders as $order)
+ if($order!=='')
+ $orders[]=$order;
+ if($orders!==array())
+ $sql.=' ORDER BY ' . implode(', ',$orders);
+
+ $sql=$builder->applyLimit($sql,$this->limit,$this->offset);
+ $command=$builder->getDbConnection()->createCommand($sql);
+ $builder->bindValues($command,$this->params);
+ return $command;
+ }
+}
+
+
+/**
+ * CStatElement represents STAT join element for {@link CActiveFinder}.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $
+ * @package system.db.ar
+ */
+class CStatElement
+{
+ /**
+ * @var CActiveRelation the relation represented by this tree node
+ */
+ public $relation;
+
+ private $_finder;
+ private $_parent;
+
+ /**
+ * Constructor.
+ * @param CActiveFinder $finder the finder
+ * @param CStatRelation $relation the STAT relation
+ * @param CJoinElement $parent the join element owning this STAT element
+ */
+ public function __construct($finder,$relation,$parent)
+ {
+ $this->_finder=$finder;
+ $this->_parent=$parent;
+ $this->relation=$relation;
+ $parent->stats[]=$this;
+ }
+
+ /**
+ * Performs the STAT query.
+ */
+ public function query()
+ {
+ if(preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
+ $this->queryManyMany($matches[1],$matches[2]);
+ else
+ $this->queryOneMany();
+ }
+
+ private function queryOneMany()
+ {
+ $relation=$this->relation;
+ $model=CActiveRecord::model($relation->className);
+ $builder=$model->getCommandBuilder();
+ $schema=$builder->getSchema();
+ $table=$model->getTableSchema();
+ $parent=$this->_parent;
+ $pkTable=$parent->model->getTableSchema();
+
+ $fks=preg_split('/\s*,\s*/',$relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
+ if(count($fks)!==count($pkTable->primaryKey))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The columns in the key must match the primary keys of the table "{table}".',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{table}'=>$pkTable->name)));
+
+ // set up mapping between fk and pk columns
+ $map=array(); // pk=>fk
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($table->columns[$fk]))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$table->name)));
+
+ if(isset($table->foreignKeys[$fk]))
+ {
+ list($tableName,$pk)=$table->foreignKeys[$fk];
+ if($schema->compareTableNames($pkTable->rawName,$tableName))
+ $map[$pk]=$fk;
+ else
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with a foreign key "{key}" that does not point to the parent table "{table}".',
+ array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$pkTable->name)));
+ }
+ else // FK constraints undefined
+ {
+ if(is_array($pkTable->primaryKey)) // composite PK
+ $map[$pkTable->primaryKey[$i]]=$fk;
+ else
+ $map[$pkTable->primaryKey]=$fk;
+ }
+ }
+
+ $records=$this->_parent->records;
+
+ $join=empty($relation->join)?'' : ' '.$relation->join;
+ $where=empty($relation->condition)?' WHERE ' : ' WHERE ('.$relation->condition.') AND ';
+ $group=empty($relation->group)?'' : ', '.$relation->group;
+ $having=empty($relation->having)?'' : ' HAVING ('.$relation->having.')';
+ $order=empty($relation->order)?'' : ' ORDER BY '.$relation->order;
+
+ $c=$schema->quoteColumnName('c');
+ $s=$schema->quoteColumnName('s');
+
+ $tableAlias=$model->getTableAlias(true);
+
+ // generate and perform query
+ if(count($fks)===1) // single column FK
+ {
+ $col=$table->columns[$fks[0]]->rawName;
+ $sql="SELECT $col AS $c, {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join
+ .$where.'('.$builder->createInCondition($table,$fks[0],array_keys($records),$tableAlias.'.').')'
+ ." GROUP BY $col".$group
+ .$having.$order;
+ $command=$builder->getDbConnection()->createCommand($sql);
+ if(is_array($relation->params))
+ $builder->bindValues($command,$relation->params);
+ $stats=array();
+ foreach($command->queryAll() as $row)
+ $stats[$row['c']]=$row['s'];
+ }
+ else // composite FK
+ {
+ $keys=array_keys($records);
+ foreach($keys as &$key)
+ {
+ $key2=unserialize($key);
+ $key=array();
+ foreach($pkTable->primaryKey as $pk)
+ $key[$map[$pk]]=$key2[$pk];
+ }
+ $cols=array();
+ foreach($pkTable->primaryKey as $n=>$pk)
+ {
+ $name=$table->columns[$map[$pk]]->rawName;
+ $cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n);
+ }
+ $sql='SELECT '.implode(', ',$cols).", {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join
+ .$where.'('.$builder->createInCondition($table,$fks,$keys,$tableAlias.'.').')'
+ .' GROUP BY '.implode(', ',array_keys($cols)).$group
+ .$having.$order;
+ $command=$builder->getDbConnection()->createCommand($sql);
+ if(is_array($relation->params))
+ $builder->bindValues($command,$relation->params);
+ $stats=array();
+ foreach($command->queryAll() as $row)
+ {
+ $key=array();
+ foreach($pkTable->primaryKey as $n=>$pk)
+ $key[$pk]=$row['c'.$n];
+ $stats[serialize($key)]=$row['s'];
+ }
+ }
+
+ // populate the results into existing records
+ foreach($records as $pk=>$record)
+ $record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$relation->defaultValue,false);
+ }
+
+ /*
+ * @param string $joinTableName jointablename
+ * @param string $keys keys
+ */
+ private function queryManyMany($joinTableName,$keys)
+ {
+ $relation=$this->relation;
+ $model=CActiveRecord::model($relation->className);
+ $table=$model->getTableSchema();
+ $builder=$model->getCommandBuilder();
+ $schema=$builder->getSchema();
+ $pkTable=$this->_parent->model->getTableSchema();
+
+ $tableAlias=$model->getTableAlias(true);
+
+ if(($joinTable=$builder->getSchema()->getTable($joinTableName))===null)
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly. The join table "{joinTable}" given in the foreign key cannot be found in the database.',
+ array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{joinTable}'=>$joinTableName)));
+
+ $fks=preg_split('/\s*,\s*/',$keys,-1,PREG_SPLIT_NO_EMPTY);
+ if(count($fks)!==count($table->primaryKey)+count($pkTable->primaryKey))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
+ array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name)));
+
+ $joinCondition=array();
+ $map=array();
+
+ $fkDefined=true;
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($joinTable->columns[$fk]))
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
+ array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name)));
+
+ if(isset($joinTable->foreignKeys[$fk]))
+ {
+ list($tableName,$pk)=$joinTable->foreignKeys[$fk];
+ if(!isset($joinCondition[$pk]) && $schema->compareTableNames($table->rawName,$tableName))
+ $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
+ else if(!isset($map[$pk]) && $schema->compareTableNames($pkTable->rawName,$tableName))
+ $map[$pk]=$fk;
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+ else
+ {
+ $fkDefined=false;
+ break;
+ }
+ }
+
+ if(!$fkDefined)
+ {
+ $joinCondition=array();
+ $map=array();
+ foreach($fks as $i=>$fk)
+ {
+ if($i<count($pkTable->primaryKey))
+ {
+ $pk=is_array($pkTable->primaryKey) ? $pkTable->primaryKey[$i] : $pkTable->primaryKey;
+ $map[$pk]=$fk;
+ }
+ else
+ {
+ $j=$i-count($pkTable->primaryKey);
+ $pk=is_array($table->primaryKey) ? $table->primaryKey[$j] : $table->primaryKey;
+ $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
+ }
+ }
+ }
+
+ if($joinCondition===array() || $map===array())
+ throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
+ array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name)));
+
+ $records=$this->_parent->records;
+
+ $cols=array();
+ foreach(is_string($pkTable->primaryKey)?array($pkTable->primaryKey):$pkTable->primaryKey as $n=>$pk)
+ {
+ $name=$joinTable->rawName.'.'.$schema->quoteColumnName($map[$pk]);
+ $cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n);
+ }
+
+ $keys=array_keys($records);
+ if(is_array($pkTable->primaryKey))
+ {
+ foreach($keys as &$key)
+ {
+ $key2=unserialize($key);
+ $key=array();
+ foreach($pkTable->primaryKey as $pk)
+ $key[$map[$pk]]=$key2[$pk];
+ }
+ }
+
+ $join=empty($relation->join)?'' : ' '.$relation->join;
+ $where=empty($relation->condition)?'' : ' WHERE ('.$relation->condition.')';
+ $group=empty($relation->group)?'' : ', '.$relation->group;
+ $having=empty($relation->having)?'' : ' AND ('.$relation->having.')';
+ $order=empty($relation->order)?'' : ' ORDER BY '.$relation->order;
+
+ $sql='SELECT '.$this->relation->select.' AS '.$schema->quoteColumnName('s').', '.implode(', ',$cols)
+ .' FROM '.$table->rawName.' '.$tableAlias.' INNER JOIN '.$joinTable->rawName
+ .' ON ('.implode(') AND (',$joinCondition).')'.$join
+ .$where
+ .' GROUP BY '.implode(', ',array_keys($cols)).$group
+ .' HAVING ('.$builder->createInCondition($joinTable,$map,$keys).')'
+ .$having.$order;
+
+ $command=$builder->getDbConnection()->createCommand($sql);
+ if(is_array($relation->params))
+ $builder->bindValues($command,$relation->params);
+
+ $stats=array();
+ foreach($command->queryAll() as $row)
+ {
+ if(is_array($pkTable->primaryKey))
+ {
+ $key=array();
+ foreach($pkTable->primaryKey as $n=>$k)
+ $key[$k]=$row['c'.$n];
+ $stats[serialize($key)]=$row['s'];
+ }
+ else
+ $stats[$row['c0']]=$row['s'];
+ }
+
+ foreach($records as $pk=>$record)
+ $record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$this->relation->defaultValue,false);
+ }
+} \ No newline at end of file