PageRenderTime 76ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 1ms

/yii/framework/db/ar/CActiveFinder.php

https://github.com/joshuaswarren/weatherhub
PHP | 1587 lines | 1163 code | 125 blank | 299 comment | 201 complexity | a8a0c8c080ef255a4a2b07151029fd1c MD5 | raw file
  1. <?php
  2. /**
  3. * CActiveRecord class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright Copyright &copy; 2008-2011 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * CActiveFinder implements eager loading and lazy loading of related active records.
  12. *
  13. * When used in eager loading, this class provides the same set of find methods as
  14. * {@link CActiveRecord}.
  15. *
  16. * @author Qiang Xue <qiang.xue@gmail.com>
  17. * @version $Id: CActiveFinder.php 3281 2011-06-15 19:05:45Z qiang.xue $
  18. * @package system.db.ar
  19. * @since 1.0
  20. */
  21. class CActiveFinder extends CComponent
  22. {
  23. /**
  24. * @var boolean join all tables all at once. Defaults to false.
  25. * This property is internally used.
  26. * @since 1.0.2
  27. */
  28. public $joinAll=false;
  29. /**
  30. * @var boolean whether the base model has limit or offset.
  31. * This property is internally used.
  32. * @since 1.0.2
  33. */
  34. public $baseLimited=false;
  35. private $_joinCount=0;
  36. private $_joinTree;
  37. private $_builder;
  38. /**
  39. * Constructor.
  40. * A join tree is built up based on the declared relationships between active record classes.
  41. * @param CActiveRecord $model the model that initiates the active finding process
  42. * @param mixed $with the relation names to be actively looked for
  43. */
  44. public function __construct($model,$with)
  45. {
  46. $this->_builder=$model->getCommandBuilder();
  47. $this->_joinTree=new CJoinElement($this,$model);
  48. $this->buildJoinTree($this->_joinTree,$with);
  49. }
  50. /**
  51. * Do not call this method. This method is used internally to perform the relational query
  52. * based on the given DB criteria.
  53. * @param CDbCriteria $criteria the DB criteria
  54. * @param boolean $all whether to bring back all records
  55. * @return mixed the query result
  56. */
  57. public function query($criteria,$all=false)
  58. {
  59. $this->joinAll=$criteria->together===true;
  60. $this->_joinTree->beforeFind(false);
  61. if($criteria->alias!='')
  62. {
  63. $this->_joinTree->tableAlias=$criteria->alias;
  64. $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($criteria->alias);
  65. }
  66. $this->_joinTree->find($criteria);
  67. $this->_joinTree->afterFind();
  68. if($all)
  69. {
  70. $result = array_values($this->_joinTree->records);
  71. if ($criteria->index!==null)
  72. {
  73. $index=$criteria->index;
  74. $array=array();
  75. foreach($result as $object)
  76. $array[$object->$index]=$object;
  77. $result=$array;
  78. }
  79. }
  80. else if(count($this->_joinTree->records))
  81. $result = reset($this->_joinTree->records);
  82. else
  83. $result = null;
  84. $this->destroyJoinTree();
  85. return $result;
  86. }
  87. /**
  88. * This method is internally called.
  89. * @param string $sql the SQL statement
  90. * @param array $params parameters to be bound to the SQL statement
  91. * @return CActiveRecord
  92. */
  93. public function findBySql($sql,$params=array())
  94. {
  95. Yii::trace(get_class($this->_joinTree->model).'.findBySql() eagerly','system.db.ar.CActiveRecord');
  96. if(($row=$this->_builder->createSqlCommand($sql,$params)->queryRow())!==false)
  97. {
  98. $baseRecord=$this->_joinTree->model->populateRecord($row,false);
  99. $this->_joinTree->beforeFind(false);
  100. $this->_joinTree->findWithBase($baseRecord);
  101. $this->_joinTree->afterFind();
  102. $this->destroyJoinTree();
  103. return $baseRecord;
  104. }
  105. else
  106. $this->destroyJoinTree();
  107. }
  108. /**
  109. * This method is internally called.
  110. * @param string $sql the SQL statement
  111. * @param array $params parameters to be bound to the SQL statement
  112. * @return CActiveRecord[]
  113. */
  114. public function findAllBySql($sql,$params=array())
  115. {
  116. Yii::trace(get_class($this->_joinTree->model).'.findAllBySql() eagerly','system.db.ar.CActiveRecord');
  117. if(($rows=$this->_builder->createSqlCommand($sql,$params)->queryAll())!==array())
  118. {
  119. $baseRecords=$this->_joinTree->model->populateRecords($rows,false);
  120. $this->_joinTree->beforeFind(false);
  121. $this->_joinTree->findWithBase($baseRecords);
  122. $this->_joinTree->afterFind();
  123. $this->destroyJoinTree();
  124. return $baseRecords;
  125. }
  126. else
  127. {
  128. $this->destroyJoinTree();
  129. return array();
  130. }
  131. }
  132. /**
  133. * This method is internally called.
  134. * @param CDbCriteria $criteria the query criteria
  135. * @return string
  136. */
  137. public function count($criteria)
  138. {
  139. Yii::trace(get_class($this->_joinTree->model).'.count() eagerly','system.db.ar.CActiveRecord');
  140. $this->joinAll=$criteria->together!==true;
  141. $alias=$criteria->alias===null ? 't' : $criteria->alias;
  142. $this->_joinTree->tableAlias=$alias;
  143. $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($alias);
  144. $n=$this->_joinTree->count($criteria);
  145. $this->destroyJoinTree();
  146. return $n;
  147. }
  148. /**
  149. * Finds the related objects for the specified active record.
  150. * This method is internally invoked by {@link CActiveRecord} to support lazy loading.
  151. * @param CActiveRecord $baseRecord the base record whose related objects are to be loaded
  152. */
  153. public function lazyFind($baseRecord)
  154. {
  155. $this->_joinTree->lazyFind($baseRecord);
  156. if(!empty($this->_joinTree->children))
  157. {
  158. $child=reset($this->_joinTree->children);
  159. $child->afterFind();
  160. }
  161. $this->destroyJoinTree();
  162. }
  163. private function destroyJoinTree()
  164. {
  165. if($this->_joinTree!==null)
  166. $this->_joinTree->destroy();
  167. $this->_joinTree=null;
  168. }
  169. /**
  170. * Builds up the join tree representing the relationships involved in this query.
  171. * @param CJoinElement $parent the parent tree node
  172. * @param mixed $with the names of the related objects relative to the parent tree node
  173. * @param array $options additional query options to be merged with the relation
  174. */
  175. private function buildJoinTree($parent,$with,$options=null)
  176. {
  177. if($parent instanceof CStatElement)
  178. throw new CDbException(Yii::t('yii','The STAT relation "{name}" cannot have child relations.',
  179. array('{name}'=>$parent->relation->name)));
  180. if(is_string($with))
  181. {
  182. if(($pos=strrpos($with,'.'))!==false)
  183. {
  184. $parent=$this->buildJoinTree($parent,substr($with,0,$pos));
  185. $with=substr($with,$pos+1);
  186. }
  187. // named scope
  188. $scopes=array();
  189. if(($pos=strpos($with,':'))!==false)
  190. {
  191. $scopes=explode(':',substr($with,$pos+1));
  192. $with=substr($with,0,$pos);
  193. }
  194. if(isset($parent->children[$with]) && $parent->children[$with]->master===null)
  195. return $parent->children[$with];
  196. if(($relation=$parent->model->getActiveRelation($with))===null)
  197. throw new CDbException(Yii::t('yii','Relation "{name}" is not defined in active record class "{class}".',
  198. array('{class}'=>get_class($parent->model), '{name}'=>$with)));
  199. $relation=clone $relation;
  200. $model=CActiveRecord::model($relation->className);
  201. if($relation instanceof CActiveRelation)
  202. {
  203. $oldAlias=$model->getTableAlias(false,false);
  204. $model->setTableAlias($relation->alias===null?$relation->name:$relation->alias);
  205. }
  206. if(($scope=$model->defaultScope())!==array())
  207. $relation->mergeWith($scope,true);
  208. if(!empty($options['scopes']))
  209. $scopes=array_merge($scopes,(array)$options['scopes']); // no need complex merging, $scopes always in simle format
  210. if($scopes!==array())
  211. {
  212. $scs=$model->scopes();
  213. foreach($scopes as $k=>$v)
  214. {
  215. if(is_integer($k))
  216. {
  217. if(is_string($v))
  218. {
  219. if(isset($scs[$v]))
  220. {
  221. $relation->mergeWith($scs[$v],true);
  222. continue;
  223. }
  224. $scope=$v;
  225. $params=array();
  226. }
  227. else if(is_array($v))
  228. {
  229. $scope=key($v);
  230. $params=current($v);
  231. }
  232. }
  233. else if(is_string($k))
  234. {
  235. $scope=$k;
  236. $params=$v;
  237. }
  238. $model->resetScope();
  239. call_user_func_array(array($model,$scope),(array)$params);
  240. $relation->mergeWith($model->getDbCriteria(),true);
  241. }
  242. }
  243. // dynamic options
  244. if($options!==null)
  245. $relation->mergeWith($options);
  246. if($relation instanceof CActiveRelation)
  247. $model->setTableAlias($oldAlias);
  248. if($relation instanceof CStatRelation)
  249. return new CStatElement($this,$relation,$parent);
  250. else
  251. {
  252. if(isset($parent->children[$with]))
  253. {
  254. $element=$parent->children[$with];
  255. $element->relation=$relation;
  256. }
  257. else
  258. $element=new CJoinElement($this,$relation,$parent,++$this->_joinCount);
  259. if(!empty($relation->through))
  260. {
  261. $slave=$this->buildJoinTree($parent,$relation->through,array('select'=>false));
  262. $slave->master=$element;
  263. $element->slave=$slave;
  264. }
  265. $parent->children[$with]=$element;
  266. if(!empty($relation->with))
  267. $this->buildJoinTree($element,$relation->with);
  268. return $element;
  269. }
  270. }
  271. // $with is an array, keys are relation name, values are relation spec
  272. foreach($with as $key=>$value)
  273. {
  274. if(is_string($value)) // the value is a relation name
  275. $this->buildJoinTree($parent,$value);
  276. else if(is_string($key) && is_array($value))
  277. $this->buildJoinTree($parent,$key,$value);
  278. }
  279. }
  280. }
  281. /**
  282. * CJoinElement represents a tree node in the join tree created by {@link CActiveFinder}.
  283. *
  284. * @author Qiang Xue <qiang.xue@gmail.com>
  285. * @version $Id: CActiveFinder.php 3281 2011-06-15 19:05:45Z qiang.xue $
  286. * @package system.db.ar
  287. * @since 1.0
  288. */
  289. class CJoinElement
  290. {
  291. /**
  292. * @var integer the unique ID of this tree node
  293. */
  294. public $id;
  295. /**
  296. * @var CActiveRelation the relation represented by this tree node
  297. */
  298. public $relation;
  299. /**
  300. * @var CActiveRelation the master relation
  301. */
  302. public $master;
  303. /**
  304. * @var CActiveRelation the slave relation
  305. */
  306. public $slave;
  307. /**
  308. * @var CActiveRecord the model associated with this tree node
  309. */
  310. public $model;
  311. /**
  312. * @var array list of active records found by the queries. They are indexed by primary key values.
  313. */
  314. public $records=array();
  315. /**
  316. * @var array list of child join elements
  317. */
  318. public $children=array();
  319. /**
  320. * @var array list of stat elements
  321. * @since 1.0.4
  322. */
  323. public $stats=array();
  324. /**
  325. * @var string table alias for this join element
  326. */
  327. public $tableAlias;
  328. /**
  329. * @var string the quoted table alias for this element
  330. */
  331. public $rawTableAlias;
  332. private $_finder;
  333. private $_builder;
  334. private $_parent;
  335. private $_pkAlias; // string or name=>alias
  336. private $_columnAliases=array(); // name=>alias
  337. private $_joined=false;
  338. private $_table;
  339. private $_related=array(); // PK, relation name, related PK => true
  340. /**
  341. * Constructor.
  342. * @param CActiveFinder $finder the finder
  343. * @param mixed $relation the relation (if the third parameter is not null)
  344. * or the model (if the third parameter is null) associated with this tree node.
  345. * @param CJoinElement $parent the parent tree node
  346. * @param integer $id the ID of this tree node that is unique among all the tree nodes
  347. */
  348. public function __construct($finder,$relation,$parent=null,$id=0)
  349. {
  350. $this->_finder=$finder;
  351. $this->id=$id;
  352. if($parent!==null)
  353. {
  354. $this->relation=$relation;
  355. $this->_parent=$parent;
  356. $this->model=CActiveRecord::model($relation->className);
  357. $this->_builder=$this->model->getCommandBuilder();
  358. $this->tableAlias=$relation->alias===null?$relation->name:$relation->alias;
  359. $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias);
  360. $this->_table=$this->model->getTableSchema();
  361. }
  362. else // root element, the first parameter is the model.
  363. {
  364. $this->model=$relation;
  365. $this->_builder=$relation->getCommandBuilder();
  366. $this->_table=$relation->getTableSchema();
  367. $this->tableAlias=$this->model->getTableAlias();
  368. $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias);
  369. }
  370. // set up column aliases, such as t1_c2
  371. $table=$this->_table;
  372. if($this->model->getDbConnection()->getDriverName()==='oci') // Issue 482
  373. $prefix='T'.$id.'_C';
  374. else
  375. $prefix='t'.$id.'_c';
  376. foreach($table->getColumnNames() as $key=>$name)
  377. {
  378. $alias=$prefix.$key;
  379. $this->_columnAliases[$name]=$alias;
  380. if($table->primaryKey===$name)
  381. $this->_pkAlias=$alias;
  382. else if(is_array($table->primaryKey) && in_array($name,$table->primaryKey))
  383. $this->_pkAlias[$name]=$alias;
  384. }
  385. }
  386. /**
  387. * Removes references to child elements and finder to avoid circular references.
  388. * This is internally used.
  389. */
  390. public function destroy()
  391. {
  392. if(!empty($this->children))
  393. {
  394. foreach($this->children as $child)
  395. $child->destroy();
  396. }
  397. unset($this->_finder, $this->_parent, $this->model, $this->relation, $this->master, $this->slave, $this->records, $this->children, $this->stats);
  398. }
  399. /**
  400. * Performs the recursive finding with the criteria.
  401. * @param CDbCriteria $criteria the query criteria
  402. */
  403. public function find($criteria=null)
  404. {
  405. if($this->_parent===null) // root element
  406. {
  407. $query=new CJoinQuery($this,$criteria);
  408. $this->_finder->baseLimited=($criteria->offset>=0 || $criteria->limit>=0);
  409. $this->buildQuery($query);
  410. $this->_finder->baseLimited=false;
  411. $this->runQuery($query);
  412. }
  413. else if(!$this->_joined && !empty($this->_parent->records)) // not joined before
  414. {
  415. $query=new CJoinQuery($this->_parent);
  416. $this->_joined=true;
  417. $query->join($this);
  418. $this->buildQuery($query);
  419. $this->_parent->runQuery($query);
  420. }
  421. foreach($this->children as $child) // find recursively
  422. $child->find();
  423. foreach($this->stats as $stat)
  424. $stat->query();
  425. }
  426. /**
  427. * Performs lazy find with the specified base record.
  428. * @param CActiveRecord $baseRecord the active record whose related object is to be fetched.
  429. */
  430. public function lazyFind($baseRecord)
  431. {
  432. if(is_string($this->_table->primaryKey))
  433. $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord;
  434. else
  435. {
  436. $pk=array();
  437. foreach($this->_table->primaryKey as $name)
  438. $pk[$name]=$baseRecord->$name;
  439. $this->records[serialize($pk)]=$baseRecord;
  440. }
  441. foreach($this->stats as $stat)
  442. $stat->query();
  443. switch(count($this->children))
  444. {
  445. case 0:
  446. return;
  447. break;
  448. case 1:
  449. $child=reset($this->children);
  450. break;
  451. default: // bridge(s) inside
  452. $child=end($this->children);
  453. break;
  454. }
  455. $query=new CJoinQuery($child);
  456. $query->selects=array();
  457. $query->selects[]=$child->getColumnSelect($child->relation->select);
  458. $query->conditions=array();
  459. $query->conditions[]=$child->relation->condition;
  460. $query->conditions[]=$child->relation->on;
  461. $query->groups[]=$child->relation->group;
  462. $query->joins[]=$child->relation->join;
  463. $query->havings[]=$child->relation->having;
  464. $query->orders[]=$child->relation->order;
  465. if(is_array($child->relation->params))
  466. $query->params=$child->relation->params;
  467. $query->elements[$child->id]=true;
  468. if($child->relation instanceof CHasManyRelation)
  469. {
  470. $query->limit=$child->relation->limit;
  471. $query->offset=$child->relation->offset;
  472. }
  473. $child->beforeFind();
  474. $child->applyLazyCondition($query,$baseRecord);
  475. $this->_joined=true;
  476. $child->_joined=true;
  477. $this->_finder->baseLimited=false;
  478. $child->buildQuery($query);
  479. $child->runQuery($query);
  480. foreach($child->children as $c)
  481. $c->find();
  482. if(empty($child->records))
  483. return;
  484. if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
  485. $baseRecord->addRelatedRecord($child->relation->name,reset($child->records),false);
  486. else // has_many and many_many
  487. {
  488. foreach($child->records as $record)
  489. {
  490. if($child->relation->index!==null)
  491. $index=$record->{$child->relation->index};
  492. else
  493. $index=true;
  494. $baseRecord->addRelatedRecord($child->relation->name,$record,$index);
  495. }
  496. }
  497. }
  498. /**
  499. * Apply Lazy Condition
  500. * @param CJoinQuery $query represents a JOIN SQL statements
  501. * @param CActiveRecord $record the active record whose related object is to be fetched.
  502. */
  503. private function applyLazyCondition($query,$record)
  504. {
  505. $schema=$this->_builder->getSchema();
  506. $parent=$this->_parent;
  507. if($this->relation instanceof CManyManyRelation)
  508. {
  509. if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
  510. 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,...)".',
  511. array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name)));
  512. if(($joinTable=$schema->getTable($matches[1]))===null)
  513. 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.',
  514. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1])));
  515. $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);
  516. $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias);
  517. $parentCondition=array();
  518. $childCondition=array();
  519. $count=0;
  520. $params=array();
  521. $fkDefined=true;
  522. foreach($fks as $i=>$fk)
  523. {
  524. if(isset($joinTable->foreignKeys[$fk])) // FK defined
  525. {
  526. list($tableName,$pk)=$joinTable->foreignKeys[$fk];
  527. if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName))
  528. {
  529. $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count;
  530. $params[':ypl'.$count]=$record->$pk;
  531. $count++;
  532. }
  533. else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName))
  534. $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  535. else
  536. {
  537. $fkDefined=false;
  538. break;
  539. }
  540. }
  541. else
  542. {
  543. $fkDefined=false;
  544. break;
  545. }
  546. }
  547. if(!$fkDefined)
  548. {
  549. $parentCondition=array();
  550. $childCondition=array();
  551. $count=0;
  552. $params=array();
  553. foreach($fks as $i=>$fk)
  554. {
  555. if($i<count($parent->_table->primaryKey))
  556. {
  557. $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
  558. $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count;
  559. $params[':ypl'.$count]=$record->$pk;
  560. $count++;
  561. }
  562. else
  563. {
  564. $j=$i-count($parent->_table->primaryKey);
  565. $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
  566. $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  567. }
  568. }
  569. }
  570. if($parentCondition!==array() && $childCondition!==array())
  571. {
  572. $join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON ';
  573. $join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')';
  574. if(!empty($this->relation->on))
  575. $join.=' AND ('.$this->relation->on.')';
  576. $query->joins[]=$join;
  577. foreach($params as $name=>$value)
  578. $query->params[$name]=$value;
  579. }
  580. else
  581. 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.',
  582. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));
  583. }
  584. else
  585. {
  586. $element=$this;
  587. while($element->slave!==null)
  588. {
  589. $fks=preg_split('/\s*,\s*/',$element->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
  590. if($element->slave->slave===null)
  591. {
  592. $fke=$element->slave;
  593. $pke=$element;
  594. }
  595. else // nested through detected
  596. {
  597. $fke=$element;
  598. $pke=$element->slave;
  599. }
  600. $query->joins[]=$element->slave->joinOneMany($fke,$fks,$pke,$parent);
  601. $element=$element->slave;
  602. }
  603. $fks=preg_split('/\s*,\s*/',$element->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
  604. $prefix=$element->getColumnPrefix();
  605. $params=array();
  606. foreach($fks as $i=>$fk)
  607. {
  608. if($this->relation instanceof CBelongsToRelation)
  609. {
  610. if(isset($parent->_table->foreignKeys[$fk])) // FK defined
  611. $pk=$parent->_table->foreignKeys[$fk][1];
  612. else if(is_array($this->_table->primaryKey)) // composite PK
  613. $pk=$this->_table->primaryKey[$i];
  614. else
  615. $pk=$this->_table->primaryKey;
  616. $params[$pk]=$record->$fk;
  617. }
  618. else
  619. {
  620. if(isset($this->_table->foreignKeys[$fk])) // FK defined
  621. $pk=$this->_table->foreignKeys[$fk][1];
  622. else if(is_array($parent->_table->primaryKey)) // composite PK
  623. $pk=$parent->_table->primaryKey[$i];
  624. else
  625. $pk=$parent->_table->primaryKey;
  626. $params[$fk]=$record->$pk;
  627. }
  628. }
  629. $count=0;
  630. foreach($params as $name=>$value)
  631. {
  632. $query->conditions[]=$prefix.$schema->quoteColumnName($name).'=:ypl'.$count;
  633. $query->params[':ypl'.$count]=$value;
  634. $count++;
  635. }
  636. }
  637. }
  638. /**
  639. * Performs the eager loading with the base records ready.
  640. * @param mixed $baseRecords the available base record(s).
  641. */
  642. public function findWithBase($baseRecords)
  643. {
  644. if(!is_array($baseRecords))
  645. $baseRecords=array($baseRecords);
  646. if(is_string($this->_table->primaryKey))
  647. {
  648. foreach($baseRecords as $baseRecord)
  649. $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord;
  650. }
  651. else
  652. {
  653. foreach($baseRecords as $baseRecord)
  654. {
  655. $pk=array();
  656. foreach($this->_table->primaryKey as $name)
  657. $pk[$name]=$baseRecord->$name;
  658. $this->records[serialize($pk)]=$baseRecord;
  659. }
  660. }
  661. $query=new CJoinQuery($this);
  662. $this->buildQuery($query);
  663. if(count($query->joins)>1)
  664. $this->runQuery($query);
  665. foreach($this->children as $child)
  666. $child->find();
  667. foreach($this->stats as $stat)
  668. $stat->query();
  669. }
  670. /**
  671. * Count the number of primary records returned by the join statement.
  672. * @param CDbCriteria $criteria the query criteria
  673. * @return string number of primary records. Note: type is string to keep max. precision.
  674. * @since 1.0.3
  675. */
  676. public function count($criteria=null)
  677. {
  678. $query=new CJoinQuery($this,$criteria);
  679. // ensure only one big join statement is used
  680. $this->_finder->baseLimited=false;
  681. $this->_finder->joinAll=true;
  682. $this->buildQuery($query);
  683. $select=is_array($criteria->select) ? implode(',',$criteria->select) : $criteria->select;
  684. if($select!=='*' && !strncasecmp($select,'count',5))
  685. $query->selects=array($select);
  686. else if(is_string($this->_table->primaryKey))
  687. {
  688. $prefix=$this->getColumnPrefix();
  689. $schema=$this->_builder->getSchema();
  690. $column=$prefix.$schema->quoteColumnName($this->_table->primaryKey);
  691. $query->selects=array("COUNT(DISTINCT $column)");
  692. }
  693. else
  694. $query->selects=array("COUNT(*)");
  695. $query->orders=$query->groups=$query->havings=array();
  696. $query->limit=$query->offset=-1;
  697. $command=$query->createCommand($this->_builder);
  698. return $command->queryScalar();
  699. }
  700. /**
  701. * Calls {@link CActiveRecord::beforeFind}.
  702. * @param boolean $isChild whether is called for a child
  703. * @since 1.0.11
  704. */
  705. public function beforeFind($isChild=true)
  706. {
  707. if($isChild)
  708. $this->model->beforeFindInternal();
  709. foreach($this->children as $child)
  710. $child->beforeFind(true);
  711. }
  712. /**
  713. * Calls {@link CActiveRecord::afterFind} of all the records.
  714. * @since 1.0.3
  715. */
  716. public function afterFind()
  717. {
  718. foreach($this->records as $record)
  719. $record->afterFindInternal();
  720. foreach($this->children as $child)
  721. $child->afterFind();
  722. $this->children = null;
  723. }
  724. /**
  725. * Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes.
  726. * @param CJoinQuery $query the query being built up
  727. */
  728. public function buildQuery($query)
  729. {
  730. foreach($this->children as $child)
  731. {
  732. if($child->master!==null)
  733. $child->_joined=true;
  734. else if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation
  735. || $this->_finder->joinAll || $child->relation->together || (!$this->_finder->baseLimited && $child->relation->together===null))
  736. {
  737. $child->_joined=true;
  738. $query->join($child);
  739. $child->buildQuery($query);
  740. }
  741. }
  742. }
  743. /**
  744. * Executes the join query and populates the query results.
  745. * @param CJoinQuery $query the query to be executed.
  746. */
  747. public function runQuery($query)
  748. {
  749. $command=$query->createCommand($this->_builder);
  750. foreach($command->queryAll() as $row)
  751. $this->populateRecord($query,$row);
  752. }
  753. /**
  754. * Populates the active records with the query data.
  755. * @param CJoinQuery $query the query executed
  756. * @param array $row a row of data
  757. * @return CActiveRecord the populated record
  758. */
  759. private function populateRecord($query,$row)
  760. {
  761. // determine the primary key value
  762. if(is_string($this->_pkAlias)) // single key
  763. {
  764. if(isset($row[$this->_pkAlias]))
  765. $pk=$row[$this->_pkAlias];
  766. else // no matching related objects
  767. return null;
  768. }
  769. else // is_array, composite key
  770. {
  771. $pk=array();
  772. foreach($this->_pkAlias as $name=>$alias)
  773. {
  774. if(isset($row[$alias]))
  775. $pk[$name]=$row[$alias];
  776. else // no matching related objects
  777. return null;
  778. }
  779. $pk=serialize($pk);
  780. }
  781. // retrieve or populate the record according to the primary key value
  782. if(isset($this->records[$pk]))
  783. $record=$this->records[$pk];
  784. else
  785. {
  786. $attributes=array();
  787. $aliases=array_flip($this->_columnAliases);
  788. foreach($row as $alias=>$value)
  789. {
  790. if(isset($aliases[$alias]))
  791. $attributes[$aliases[$alias]]=$value;
  792. }
  793. $record=$this->model->populateRecord($attributes,false);
  794. foreach($this->children as $child)
  795. {
  796. if(!empty($child->relation->select))
  797. $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation);
  798. }
  799. $this->records[$pk]=$record;
  800. }
  801. // populate child records recursively
  802. foreach($this->children as $child)
  803. {
  804. if(!isset($query->elements[$child->id]) || empty($child->relation->select))
  805. continue;
  806. $childRecord=$child->populateRecord($query,$row);
  807. if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
  808. $record->addRelatedRecord($child->relation->name,$childRecord,false);
  809. else // has_many and many_many
  810. {
  811. // need to double check to avoid adding duplicated related objects
  812. if($childRecord instanceof CActiveRecord)
  813. $fpk=serialize($childRecord->getPrimaryKey());
  814. else
  815. $fpk=0;
  816. if(!isset($this->_related[$pk][$child->relation->name][$fpk]))
  817. {
  818. if($childRecord instanceof CActiveRecord && $child->relation->index!==null)
  819. $index=$childRecord->{$child->relation->index};
  820. else
  821. $index=true;
  822. $record->addRelatedRecord($child->relation->name,$childRecord,$index);
  823. $this->_related[$pk][$child->relation->name][$fpk]=true;
  824. }
  825. }
  826. }
  827. return $record;
  828. }
  829. /**
  830. * @return string the table name and the table alias (if any). This can be used directly in SQL query without escaping.
  831. */
  832. public function getTableNameWithAlias()
  833. {
  834. if($this->tableAlias!==null)
  835. return $this->_table->rawName . ' ' . $this->rawTableAlias;
  836. else
  837. return $this->_table->rawName;
  838. }
  839. /**
  840. * Generates the list of columns to be selected.
  841. * Columns will be properly aliased and primary keys will be added to selection if they are not specified.
  842. * @param mixed $select columns to be selected. Defaults to '*', indicating all columns.
  843. * @return string the column selection
  844. */
  845. public function getColumnSelect($select='*')
  846. {
  847. $schema=$this->_builder->getSchema();
  848. $prefix=$this->getColumnPrefix();
  849. $columns=array();
  850. if($select==='*')
  851. {
  852. foreach($this->_table->getColumnNames() as $name)
  853. $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_columnAliases[$name]);
  854. }
  855. else
  856. {
  857. if(is_string($select))
  858. $select=explode(',',$select);
  859. $selected=array();
  860. foreach($select as $name)
  861. {
  862. $name=trim($name);
  863. $matches=array();
  864. if(($pos=strrpos($name,'.'))!==false)
  865. $key=substr($name,$pos+1);
  866. else
  867. $key=$name;
  868. $key=trim($key,'\'"`');
  869. if($key==='*')
  870. {
  871. foreach($this->_table->columns as $name=>$column)
  872. {
  873. $alias=$this->_columnAliases[$name];
  874. if(!isset($selected[$alias]))
  875. {
  876. $columns[]=$prefix.$column->rawName.' AS '.$schema->quoteColumnName($alias);
  877. $selected[$alias]=1;
  878. }
  879. }
  880. continue;
  881. }
  882. if(isset($this->_columnAliases[$key])) // simple column names
  883. {
  884. $columns[]=$prefix.$schema->quoteColumnName($key).' AS '.$schema->quoteColumnName($this->_columnAliases[$key]);
  885. $selected[$this->_columnAliases[$key]]=1;
  886. }
  887. else if(preg_match('/^(.*?)\s+AS\s+(\w+)$/im',$name,$matches)) // if the column is already aliased
  888. {
  889. $alias=$matches[2];
  890. if(!isset($this->_columnAliases[$alias]) || $this->_columnAliases[$alias]!==$alias)
  891. {
  892. $this->_columnAliases[$alias]=$alias;
  893. $columns[]=$name;
  894. $selected[$alias]=1;
  895. }
  896. }
  897. else
  898. 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.',
  899. array('{class}'=>get_class($this->model), '{column}'=>$name)));
  900. }
  901. // add primary key selection if they are not selected
  902. if(is_string($this->_pkAlias) && !isset($selected[$this->_pkAlias]))
  903. $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias);
  904. else if(is_array($this->_pkAlias))
  905. {
  906. foreach($this->_table->primaryKey as $name)
  907. if(!isset($selected[$name]))
  908. $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_pkAlias[$name]);
  909. }
  910. }
  911. return implode(', ',$columns);
  912. }
  913. /**
  914. * @return string the primary key selection
  915. */
  916. public function getPrimaryKeySelect()
  917. {
  918. $schema=$this->_builder->getSchema();
  919. $prefix=$this->getColumnPrefix();
  920. $columns=array();
  921. if(is_string($this->_pkAlias))
  922. $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias);
  923. else if(is_array($this->_pkAlias))
  924. {
  925. foreach($this->_pkAlias as $name=>$alias)
  926. $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($alias);
  927. }
  928. return implode(', ',$columns);
  929. }
  930. /**
  931. * @return string the condition that specifies only the rows with the selected primary key values.
  932. */
  933. public function getPrimaryKeyRange()
  934. {
  935. if(empty($this->records))
  936. return '';
  937. $values=array_keys($this->records);
  938. if(is_array($this->_table->primaryKey))
  939. {
  940. foreach($values as &$value)
  941. $value=unserialize($value);
  942. }
  943. return $this->_builder->createInCondition($this->_table,$this->_table->primaryKey,$values,$this->getColumnPrefix());
  944. }
  945. /**
  946. * @return string the column prefix for column reference disambiguation
  947. */
  948. public function getColumnPrefix()
  949. {
  950. if($this->tableAlias!==null)
  951. return $this->rawTableAlias.'.';
  952. else
  953. return $this->_table->rawName.'.';
  954. }
  955. /**
  956. * @return string the join statement (this node joins with its parent)
  957. */
  958. public function getJoinCondition()
  959. {
  960. $parent=$this->_parent;
  961. if($this->relation instanceof CManyManyRelation)
  962. {
  963. if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
  964. 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,...)".',
  965. array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name)));
  966. $schema=$this->_builder->getSchema();
  967. if(($joinTable=$schema->getTable($matches[1]))===null)
  968. 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.',
  969. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1])));
  970. $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);
  971. return $this->joinManyMany($joinTable,$fks,$parent);
  972. }
  973. else
  974. {
  975. $fks=preg_split('/\s*,\s*/',$this->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
  976. if($this->relation instanceof CBelongsToRelation)
  977. {
  978. $pke=$this;
  979. $fke=$parent;
  980. }
  981. else if($this->slave===null)
  982. {
  983. $pke=$parent;
  984. $fke=$this;
  985. }
  986. else if($this->slave->slave===null)
  987. {
  988. $pke=$this;
  989. $fke=$this->slave;
  990. }
  991. else // nested through detected
  992. {
  993. $pke=$this->slave;
  994. $fke=$this;
  995. }
  996. return $this->joinOneMany($fke,$fks,$pke,$parent);
  997. }
  998. }
  999. /**
  1000. * Generates the join statement for one-many relationship.
  1001. * This works for HAS_ONE, HAS_MANY and BELONGS_TO.
  1002. * @param CJoinElement $fke the join element containing foreign keys
  1003. * @param array $fks the foreign keys
  1004. * @param CJoinElement $pke the join element containg primary keys
  1005. * @param CJoinElement $parent the parent join element
  1006. * @return string the join statement
  1007. * @throws CDbException if a foreign key is invalid
  1008. */
  1009. private function joinOneMany($fke,$fks,$pke,$parent)
  1010. {
  1011. $schema=$this->_builder->getSchema();
  1012. $joins=array();
  1013. foreach($fks as $i=>$fk)
  1014. {
  1015. if(!isset($fke->_table->columns[$fk]))
  1016. 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}".',
  1017. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$fke->_table->name)));
  1018. if(isset($fke->_table->foreignKeys[$fk]) && $schema->compareTableNames($pke->_table->rawName, $fke->_table->foreignKeys[$fk][0]))
  1019. $pk=$fke->_table->foreignKeys[$fk][1];
  1020. else // FK constraints undefined
  1021. {
  1022. if(is_array($pke->_table->primaryKey)) // composite PK
  1023. $pk=$pke->_table->primaryKey[$i];
  1024. else
  1025. $pk=$pke->_table->primaryKey;
  1026. }
  1027. $joins[]=$fke->getColumnPrefix().$schema->quoteColumnName($fk) . '=' . $pke->getColumnPrefix().$schema->quoteColumnName($pk);
  1028. }
  1029. if(!empty($this->relation->on))
  1030. $joins[]=$this->relation->on;
  1031. return $this->relation->joinType . ' ' . $this->getTableNameWithAlias() . ' ON (' . implode(') AND (',$joins).')';
  1032. }
  1033. /**
  1034. * Generates the join statement for many-many relationship.
  1035. * @param CDbTableSchema $joinTable the join table
  1036. * @param array $fks the foreign keys
  1037. * @param CJoinElement $parent the parent join element
  1038. * @return string the join statement
  1039. * @throws CDbException if a foreign key is invalid
  1040. */
  1041. private function joinManyMany($joinTable,$fks,$parent)
  1042. {
  1043. $schema=$this->_builder->getSchema();
  1044. $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias);
  1045. $parentCondition=array();
  1046. $childCondition=array();
  1047. $fkDefined=true;
  1048. foreach($fks as $i=>$fk)
  1049. {
  1050. if(!isset($joinTable->columns[$fk]))
  1051. 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}".',
  1052. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name)));
  1053. if(isset($joinTable->foreignKeys[$fk]))
  1054. {
  1055. list($tableName,$pk)=$joinTable->foreignKeys[$fk];
  1056. if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName))
  1057. $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  1058. else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName))
  1059. $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  1060. else
  1061. {
  1062. $fkDefined=false;
  1063. break;
  1064. }
  1065. }
  1066. else
  1067. {
  1068. $fkDefined=false;
  1069. break;
  1070. }
  1071. }
  1072. if(!$fkDefined)
  1073. {
  1074. $parentCondition=array();
  1075. $childCondition=array();
  1076. foreach($fks as $i=>$fk)
  1077. {
  1078. if($i<count($parent->_table->primaryKey))
  1079. {
  1080. $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
  1081. $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  1082. }
  1083. else
  1084. {
  1085. $j=$i-count($parent->_table->primaryKey);
  1086. $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
  1087. $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk);
  1088. }
  1089. }
  1090. }
  1091. if($parentCondition!==array() && $childCondition!==array())
  1092. {
  1093. $join=$this->relation->joinType.' '.$joinTable->rawName.' '.$joinAlias;
  1094. $join.=' ON ('.implode(') AND (',$parentCondition).')';
  1095. $join.=' '.$this->relation->joinType.' '.$this->getTableNameWithAlias();
  1096. $join.=' ON ('.implode(') AND (',$childCondition).')';
  1097. if(!empty($this->relation->on))
  1098. $join.=' AND ('.$this->relation->on.')';
  1099. return $join;
  1100. }
  1101. else
  1102. 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.',
  1103. array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));
  1104. }
  1105. }
  1106. /**
  1107. * CJoinQuery represents a JOIN SQL statement.
  1108. *
  1109. * @author Qiang Xue <qiang.xue@gmail.com>
  1110. * @version $Id: CActiveFinder.php 3281 2011-06-15 19:05:45Z qiang.xue $
  1111. * @package system.db.ar
  1112. * @since 1.0
  1113. */
  1114. class CJoinQuery
  1115. {
  1116. /**
  1117. * @var array list of column selections
  1118. */
  1119. public $selects=array();
  1120. /**
  1121. * @var boolean whether to select distinct result set
  1122. * @since 1.0.9
  1123. */
  1124. public $distinct=false;
  1125. /**
  1126. * @var array list of join statement
  1127. */
  1128. public $joins=array();
  1129. /**
  1130. * @var array list of WHERE clauses
  1131. */
  1132. public $conditions=array();
  1133. /**
  1134. * @var array list of ORDER BY clauses
  1135. */
  1136. public $orders=array();
  1137. /**
  1138. * @var array list of GROUP BY clauses
  1139. */
  1140. public $groups=array();
  1141. /**
  1142. * @var array list of HAVING clauses
  1143. */
  1144. public $havings=array();
  1145. /**
  1146. * @var integer row limit
  1147. */
  1148. public $limit=-1;
  1149. /**
  1150. * @var integer row offset
  1151. */
  1152. public $offset=-1;
  1153. /**
  1154. * @var array list of query parameters
  1155. */
  1156. public $params=array();
  1157. /**
  1158. * @var array list of join element IDs (id=>true)
  1159. */
  1160. public $elements=array();
  1161. /**
  1162. * Constructor.
  1163. * @param CJoinElement $joinElement The root join tree.
  1164. * @param CDbCriteria $criteria the query criteria
  1165. */
  1166. public function __construct($joinElement,$criteria=null)
  1167. {
  1168. if($criteria!==null)
  1169. {
  1170. $this->selects[]=$joinElement->getColumnSelect($criteria->select);
  1171. $this->joins[]=$joinElement->getTableNameWithAlias();
  1172. $this->joins[]=$criteria->join;
  1173. $this->conditions[]=$criteria->condition;
  1174. $this->orders[]=$criteria->order;
  1175. $this->groups[]=$criteria->group;
  1176. $this->havings[]=$criteria->having;
  1177. $this->limit=$criteria->limit;
  1178. $this->offset=$criteria->offset;
  1179. $this->params=$criteria->params;
  1180. if(!$this->distinct && $criteria->distinct)
  1181. $this->distinct=true;
  1182. }
  1183. else
  1184. {
  1185. $this->selects[]=$joinElement->getPrimaryKeySelect();
  1186. $this->joins[]=$joinElement->getTableNameWithAlias();
  1187. $this->conditions[]=$joinElement->getPrimaryKeyRange();
  1188. }
  1189. $this->elements[$joinElement->id]=true;
  1190. }
  1191. /**
  1192. * Joins with another join element
  1193. * @param CJoinElement $element the element to be joined
  1194. */
  1195. public function join($element)
  1196. {
  1197. if($element->slave!==null)
  1198. $this->join($element->slave);
  1199. if(!empty($element->relation->select))
  1200. $this->selects[]=$element->getColumnSelect($element->relation->select);
  1201. $this->conditions[]=$element->relation->condition;
  1202. $this->orders[]=$element->relation->order;
  1203. $this->joins[]=$element->getJoinCondition();
  1204. $this->joins[]=$element->relation->join;
  1205. $this->groups[]=$element->relation->group;
  1206. $this->havings[]=$element->relation->having;
  1207. if(is_array($element->relation->params))
  1208. {
  1209. if(is_array($this->params))
  1210. $this->params=array_merge($this->params,$element->relation->params);
  1211. else
  1212. $this->params=$element->relation->params;
  1213. }
  1214. $this->elements[$element->id]=true;
  1215. }
  1216. /**
  1217. * Creates the SQL statement.
  1218. * @param CDbCommandBuilder $builder the command builder
  1219. * @return string the SQL statement
  1220. */
  1221. public function createCommand($builder)
  1222. {
  1223. $sql=($this->distinct ? 'SELECT DISTINCT ':'SELECT ') . implode(', ',$this->selects);
  1224. $sql.=' FROM ' . implode(' ',$this->joins);
  1225. $conditions=array();
  1226. foreach($this->conditions as $condition)
  1227. if($condition!=='')
  1228. $conditions[]=$condition;
  1229. if($conditions!==array())
  1230. $sql.=' WHERE (' . implode(') AND (',$conditions).')';
  1231. $groups=array();
  1232. foreach($this->groups as $group)
  1233. if($group!=='')
  1234. $groups[]=$group;
  1235. if($groups!==array())
  1236. $sql.=' GROUP BY ' . implode(', ',$groups);
  1237. $havings=array();
  1238. foreach($this->havings as $having)
  1239. if($having!=='')
  1240. $havings[]=$having;
  1241. if($havings!==array())
  1242. $sql.=' HAVING (' . implode(') AND (',$havings).')';
  1243. $orders=array();
  1244. foreach($this->orders as $order)
  1245. if($order!=='')
  1246. $orders[]=$order;
  1247. if($orders!==array())
  1248. $sql.=' ORDER BY ' . implode(', ',$orders);
  1249. $sql=$builder->applyLimit($sql,$this->limit,$this->offset);
  1250. $command=$builder->getDbConnection()->createCommand($sql);
  1251. $builder->bindValues($command,$this->params);
  1252. return $command;
  1253. }
  1254. }
  1255. /**
  1256. * CStatElement represents STAT join element for {@link CActiveFinder}.
  1257. *
  1258. * @author Qiang Xue <qiang.xue@gmail.com>
  1259. * @version $Id: CActiveFinder.php 3281 2011-06-15 19:05:45Z qiang.xue $
  1260. * @package system.db.ar
  1261. * @since 1.0.4
  1262. */
  1263. class CStatElement
  1264. {
  1265. /**
  1266. * @var CActiveRelation the relation represented by this tree node
  1267. */
  1268. public $relation;
  1269. private $_finder;
  1270. private $_parent;
  1271. /**
  1272. * Constructor.
  1273. * @param CActiveFinder $finder the finder
  1274. * @param CStatRelation $relation the STAT relation
  1275. * @param CJoinElement $parent the join element owning this STAT element
  1276. */
  1277. public function __construct($finder,$relation,$parent)
  1278. {
  1279. $this->_finder=$finder;
  1280. $this->_parent=$parent;
  1281. $this->relation=$relation;
  1282. $parent->stats[]=$this;
  1283. }
  1284. /**
  1285. * Performs the STAT query.
  1286. */
  1287. public function query()
  1288. {
  1289. if(preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
  1290. $this->queryManyMany($matches[1],$matches[2]);
  1291. else
  1292. $this->queryOneMany();
  1293. }
  1294. private function queryOneMany()
  1295. {
  1296. $relation=$this->relation;
  1297. $model=CActiveRecord::model($relation->className);
  1298. $builder=$model->getCommandBuilder();
  1299. $schema=$builder->getSchema();
  1300. $table=$model->getTableSchema();
  1301. $parent=$this->_parent;
  1302. $pkTable=$parent->model->getTableSchema();
  1303. $fks=preg_split('/\s*,\s*/',$relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
  1304. if(count($fks)!==count($pkTable->primaryKey))
  1305. 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}".',
  1306. array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{table}'=>$pkTable->name)));
  1307. // set up mapping between fk and pk columns
  1308. $map=array(); // pk=>fk
  1309. foreach($fks as $i=>$fk)
  1310. {
  1311. if(!isset($table->columns[$fk]))
  1312. 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}".',
  1313. array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$table->name)));
  1314. if(isset($table->foreignKeys[$fk]))
  1315. {
  1316. list($tableName,$pk)=$table->foreignKeys[$fk];
  1317. if($schema->compareTableNames($pkTable->rawName,$tableName))
  1318. $map[$pk]=$fk;
  1319. else
  1320. 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}".',
  1321. array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$pkTable->name)));
  1322. }
  1323. else // FK constraints undefined
  1324. {
  1325. if(is_array($pkTable->primaryKey)) // composite PK
  1326. $map[$pkTable->primaryKey[$i]]=$fk;
  1327. else
  1328. $map[$pkTable->primaryKey]=$fk;
  1329. }
  1330. }
  1331. $records=$this->_parent->records;
  1332. $join=empty($relation->join)?'' : ' '.$relation->join;
  1333. $where=empty($relation->condition)?' WHERE ' : ' WHERE ('.$relation->condition.') AND ';
  1334. $group=empty($relation->group)?'' : ', '.$relation->group;
  1335. $having=empty($relation->having)?'' : ' HAVING ('.$relation->having.')';
  1336. $order=empty($relation->order)?'' : ' ORDER BY '.$relation->order;
  1337. $c=$schema->quoteColumnName('c');
  1338. $s=$schema->quoteColumnName('s');
  1339. $tableAlias=$model->getTableAlias(true);
  1340. // generate and perform query
  1341. if(count($fks)===1) // single column FK
  1342. {
  1343. $col=$table->columns[$fks[0]]->rawName;
  1344. $sql="SELECT $col AS $c, {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join
  1345. .$where.'('.$builder->createInCondition($table,$fks[0],array_keys($records),$tableAlias.'.').')'
  1346. ." GROUP BY $col".$group
  1347. .$having.$order;
  1348. $command=$builder->getDbConnection()->createCommand($sql);
  1349. if(is_array($relation->params))
  1350. $builder->bindValues($command,$relation->params);
  1351. $stats=array();
  1352. foreach($command->queryAll() as $row)
  1353. $stats[$row['c']]=$row['s'];
  1354. }
  1355. else // composite FK
  1356. {
  1357. $keys=array_keys($records);
  1358. foreach($keys as &$key)
  1359. {
  1360. $key2=unserialize($key);
  1361. $key=array();
  1362. foreach($pkTable->primaryKey as $pk)
  1363. $key[$map[$pk]]=$key2[$pk];
  1364. }
  1365. $cols=array();
  1366. foreach($pkTable->primaryKey as $n=>$pk)
  1367. {
  1368. $name=$table->columns[$map[$pk]]->rawName;
  1369. $cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n);
  1370. }
  1371. $sql='SELECT '.implode(', ',$cols).", {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join
  1372. .$where.'('.$builder->createInCondition($table,$fks,$keys,$tableAlias.'.').')'
  1373. .' GROUP BY '.implode(', ',array_keys($cols)).$group
  1374. .$having.$order;
  1375. $command=$builder->getDbConnection()->createCommand($sql);
  1376. if(is_array($relation->params))
  1377. $builder->bindValues($command,$relation->params);
  1378. $stats=array();
  1379. foreach($command->queryAll() as $row)
  1380. {
  1381. $key=array();
  1382. foreach($pkTable->primaryKey as $n=>$pk)
  1383. $key[$pk]=$row['c'.$n];
  1384. $stats[serialize($key)]=$row['s'];
  1385. }
  1386. }
  1387. // populate the results into existing records
  1388. foreach($records as $pk=>$record)
  1389. $record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$relation->defaultValue,false);
  1390. }
  1391. /*
  1392. * @param string $joinTableName jointablename
  1393. * @param string $keys keys
  1394. */
  1395. private function queryManyMany($joinTableName,$keys)
  1396. {
  1397. $relation=$this->relation;
  1398. $model=CActiveRecord::model($relation->className);
  1399. $table=$model->getTableSchema();
  1400. $builder=$model->getCommandBuilder();
  1401. $schema=$builder->getSchema();
  1402. $pkTable=$this->_parent->model->getTableSchema();
  1403. $tableAlias=$model->getTableAlias(true);
  1404. if(($joinTable=$builder->getSchema()->getTable($joinTableName))===null)
  1405. 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.',
  1406. array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{joinTable}'=>$joinTableName)));
  1407. $fks=preg_split('/\s*,\s*/',$keys,-1,PREG_SPLIT_NO_EMPTY);
  1408. if(count($fks)!==count($table->primaryKey)+count($pkTable->primaryKey))
  1409. 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.',
  1410. array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name)));
  1411. $joinCondition=array();
  1412. $map=array();
  1413. $fkDefined=true;
  1414. foreach($fks as $i=>$fk)
  1415. {
  1416. if(!isset($joinTable->columns[$fk]))
  1417. 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}".',
  1418. array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name)));
  1419. if(isset($joinTable->foreignKeys[$fk]))
  1420. {
  1421. list($tableName,$pk)=$joinTable->foreignKeys[$fk];
  1422. if(!isset($joinCondition[$pk]) && $schema->compareTableNames($table->rawName,$tableName))
  1423. $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
  1424. else if(!isset($map[$pk]) && $schema->compareTableNames($pkTable->rawName,$tableName))
  1425. $map[$pk]=$fk;
  1426. else
  1427. {
  1428. $fkDefined=false;
  1429. break;
  1430. }
  1431. }
  1432. else
  1433. {
  1434. $fkDefined=false;
  1435. break;
  1436. }
  1437. }
  1438. if(!$fkDefined)
  1439. {
  1440. $joinCondition=array();
  1441. $map=array();
  1442. foreach($fks as $i=>$fk)
  1443. {
  1444. if($i<count($pkTable->primaryKey))
  1445. {
  1446. $pk=is_array($pkTable->primaryKey) ? $pkTable->primaryKey[$i] : $pkTable->primaryKey;
  1447. $map[$pk]=$fk;
  1448. }
  1449. else
  1450. {
  1451. $j=$i-count($pkTable->primaryKey);
  1452. $pk=is_array($table->primaryKey) ? $table->primaryKey[$j] : $table->primaryKey;
  1453. $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
  1454. }
  1455. }
  1456. }
  1457. if($joinCondition===array() || $map===array())
  1458. 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.',
  1459. array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name)));
  1460. $records=$this->_parent->records;
  1461. $cols=array();
  1462. foreach(is_string($pkTable->primaryKey)?array($pkTable->primaryKey):$pkTa