CActiveFinder-php72.php 64 KB

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