PageRenderTime 49ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/framework/db/ar/CActiveFinder.php

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