CActiveFinder.php 50 KB

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