* @link http://www.yiiframework.com/ * @copyright 2008-2013 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 * @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; 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; } } elseif(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->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->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)) { foreach($this->_joinTree->children as $child) $child->afterFind(); } $this->destroyJoinTree(); } /** * Given active record class name returns new model instance. * * @param string $className active record class name * @return CActiveRecord active record model instance * * @since 1.1.14 */ public function getModel($className) { return CActiveRecord::model($className); } 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 * @throws CDbException if given parent tree node is an instance of {@link CStatElement} * or relation is not defined in the given parent's tree node model class */ 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=$this->getModel($relation->className); if($relation instanceof CActiveRelation) { $oldAlias=$model->getTableAlias(false,false); if(isset($options['alias'])) $model->setTableAlias($options['alias']); elseif($relation->alias===null) $model->setTableAlias($relation->name); else $model->setTableAlias($relation->alias); } 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 $model->resetScope(false); $criteria=$model->getDbCriteria(); $criteria->scopes=$scopes; $model->beforeFindInternal(); $model->applyScopes($criteria); // select has a special meaning in stat relation, so we need to ignore select from scope or model criteria if($relation instanceof CStatRelation) $criteria->select='*'; $relation->mergeWith($criteria,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'=>'')); $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); elseif(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 * @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=$this->_finder->getModel($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; elseif(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); } elseif(!$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(); if(!$this->children) return; $params=array(); foreach($this->children as $child) if(is_array($child->relation->params)) $params=array_merge($params,$child->relation->params); $query=new CJoinQuery($child); $query->selects=array($child->getColumnSelect($child->relation->select)); $query->conditions=array( $child->relation->condition, $child->relation->on, ); $query->groups[]=$child->relation->group; $query->joins[]=$child->relation->join; $query->havings[]=$child->relation->having; $query->orders[]=$child->relation->order; $query->params=$params; $query->elements[$child->id]=true; if($child->relation instanceof CHasManyRelation) { $query->limit=$child->relation->limit; $query->offset=$child->relation->offset; } $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. * @throws CDbException if relation in active record class is not specified correctly */ private function applyLazyCondition($query,$record) { $schema=$this->_builder->getSchema(); $parent=$this->_parent; if($this->relation instanceof CManyManyRelation) { $joinTableName=$this->relation->getJunctionTableName(); if(($joinTable=$schema->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($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$joinTableName))); $fks=$this->relation->getJunctionForeignKeys(); $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++; } elseif(!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_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(true) { $condition=$element->relation->condition; if(!empty($condition)) $query->conditions[]=$condition; $query->params=array_merge($query->params,$element->relation->params); if($element->slave!==null) { $query->joins[]=$element->slave->joinOneMany($element->slave,$element->relation->foreignKey,$element,$parent); $element=$element->slave; } else break; } $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($element->relation instanceof CBelongsToRelation) { if(is_int($i)) { if(isset($parent->_table->foreignKeys[$fk])) // FK defined $pk=$parent->_table->foreignKeys[$fk][1]; elseif(is_array($element->_table->primaryKey)) // composite PK $pk=$element->_table->primaryKey[$i]; else $pk=$element->_table->primaryKey; } $params[$pk]=$record->$fk; } else { if(is_int($i)) { if(isset($element->_table->foreignKeys[$fk])) // FK defined $pk=$element->_table->foreignKeys[$fk][1]; elseif(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); $query->limit=$query->offset=-1; if(!empty($criteria->group) || !empty($criteria->having)) { $query->orders = array(); $command=$query->createCommand($this->_builder); $sql=$command->getText(); $sql="SELECT COUNT(*) FROM ({$sql}) sq"; $command->setText($sql); $command->params=$query->params; return $command->queryScalar(); } else { $select=is_array($criteria->select) ? implode(',',$criteria->select) : $criteria->select; if($select!=='*' && !strncasecmp($select,'count',5)) $query->selects=array($select); elseif(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(); $command=$query->createCommand($this->_builder); return $command->queryScalar(); } } /** * 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; elseif($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. * @throws CDbException if active record class is trying to select an invalid column * @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; } elseif(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); elseif(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); elseif(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.'.'; } /** * @throws CDbException if relation in active record class is not specified correctly * @return string the join statement (this node joins with its parent) */ public function getJoinCondition() { $parent=$this->_parent; if($this->relation instanceof CManyManyRelation) { $schema=$this->_builder->getSchema(); $joinTableName=$this->relation->getJunctionTableName(); if(($joinTable=$schema->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($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$joinTableName))); $fks=$this->relation->getJunctionForeignKeys(); 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->slave!==null) { if($this->relation instanceof CBelongsToRelation) { $fks=array_flip($fks); $pke=$this->slave; $fke=$this; } else { $pke=$this; $fke=$this->slave; } } elseif($this->relation instanceof CBelongsToRelation) { $pke=$this; $fke=$parent; } else { $pke=$parent; $fke=$this; } 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 contains 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); elseif(!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_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 * @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 CDbCommand DB command instance representing 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 * @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=$this->_finder->getModel($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=$tableAlias.'.'.$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=$tableAlias.'.'.$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=$this->_finder->getModel($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); elseif(!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($iprimaryKey)) { $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); } }