diff --git a/framework/db/ar/ActiveFinder.php b/framework/db/ar/ActiveFinder.php index 080ea53..b33b23f 100644 --- a/framework/db/ar/ActiveFinder.php +++ b/framework/db/ar/ActiveFinder.php @@ -31,7 +31,6 @@ use yii\db\Exception; * todo: lazy loading * todo: scope * todo: test via option - * todo: count, sum, exists todo: inner join with one or multiple relations as filters joinType should default to inner join in this case * @@ -61,7 +60,7 @@ class ActiveFinder extends \yii\base\Object * @param bool $all * @return array */ - public function findRecords($query, $all = true) + public function findRecords($query) { if ($query->sql !== null) { $sql = $query->sql; @@ -81,16 +80,7 @@ class ActiveFinder extends \yii\base\Object } $command = $this->connection->createCommand($sql, $query->params); - if ($all) { - $rows = $command->queryAll(); - } else { - $row = $command->queryRow(); - if ($row === false) { - return array(); - } - $rows = array($row); - } - + $rows = $command->queryAll(); $records = array(); if ($query->asArray) { if ($query->indexBy === null) { @@ -120,20 +110,18 @@ class ActiveFinder extends \yii\base\Object } - public function findRecordsWithRelations() + public function findRecordsWithRelations($query) { - if (!empty($this->with)) { - // todo: handle findBySql() and limit cases - $joinTree = $this->buildRelationalQuery(); - } + // todo: handle findBySql() and limit cases + $joinTree = $this->buildRelationalQuery(); if ($this->sql === null) { - $this->initFrom($this->query); - $command = $this->query->createCommand($this->getDbConnection()); + $this->initFrom($element->query); + $command = $element->query->createCommand($this->getDbConnection()); $this->sql = $command->getSql(); } else { $command = $this->getDbConnection()->createCommand($this->sql); - $command->bindValues($this->query->params); + $command->bindValues($element->query->params); } $rows = $command->queryAll(); @@ -204,43 +192,49 @@ class ActiveFinder extends \yii\base\Object } } - protected function buildRelationalQuery() + private $_joinCount; + private $_tableAliases; + + protected function buildQuery() { - $joinTree = new JoinElement($this, null, null); - $this->buildJoinTree($joinTree, $this->with); + $this->_joinCount = 0; + $joinTree = new JoinElement($this->_joinCount++, $element->query, null, null); + $this->buildJoinTree($joinTree, $element->query->with); + $this->_tableAliases = array(); $this->buildTableAlias($joinTree); + $query = new Query; foreach ($joinTree->children as $child) { $child->buildQuery($query); } - $select = $joinTree->buildSelect($this->query->select); + $select = $joinTree->buildSelect($element, $element->query->select); if (!empty($query->select)) { - $this->query->select = array_merge($select, $query->select); + $element->query->select = array_merge($select, $query->select); } else { - $this->query->select = $select; + $element->query->select = $select; } if (!empty($query->where)) { - $this->query->andWhere('(' . implode(') AND (', $query->where) . ')'); + $element->query->andWhere('(' . implode(') AND (', $query->where) . ')'); } if (!empty($query->having)) { - $this->query->andHaving('(' . implode(') AND (', $query->having) . ')'); + $element->query->andHaving('(' . implode(') AND (', $query->having) . ')'); } if (!empty($query->join)) { - if ($this->query->join === null) { - $this->query->join = $query->join; + if ($element->query->join === null) { + $element->query->join = $query->join; } else { - $this->query->join = array_merge($this->query->join, $query->join); + $element->query->join = array_merge($element->query->join, $query->join); } } if (!empty($query->orderBy)) { - $this->query->addOrderBy($query->orderBy); + $element->query->addOrderBy($query->orderBy); } if (!empty($query->groupBy)) { - $this->query->addGroupBy($query->groupBy); + $element->query->addGroupBy($query->groupBy); } if (!empty($query->params)) { - $this->query->addParams($query->params); + $element->query->addParams($query->params); } return $joinTree; @@ -257,10 +251,10 @@ class ActiveFinder extends \yii\base\Object { if (is_array($with)) { foreach ($with as $name => $value) { - if (is_string($value)) { - $this->buildJoinTree($parent, $value); - } elseif (is_string($name) && is_array($value)) { + if (is_array($value)) { $this->buildJoinTree($parent, $name, $value); + } else { + $this->buildJoinTree($parent, $value); } } return null; @@ -275,38 +269,173 @@ class ActiveFinder extends \yii\base\Object $child = $parent->children[$with]; $child->joinOnly = false; } else { - $modelClass = $parent->relation->modelClass; + $modelClass = $parent->query->modelClass; $relations = $modelClass::getMetaData()->relations; if (!isset($relations[$with])) { throw new Exception("$modelClass has no relation named '$with'."); } $relation = clone $relations[$with]; if ($relation->via !== null && isset($relations[$relation->via])) { - $relation->via = null; $parent2 = $this->buildJoinTree($parent, $relation->via); + $relation->via = null; if ($parent2->joinOnly === null) { $parent2->joinOnly = true; } - $child = new JoinElement($relation, $parent2, $parent); + $child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent); } else { - $child = new JoinElement($relation, $parent, $parent); + $child = new JoinElement($this->_joinCount++, $relation, $parent, $parent); } } foreach ($config as $name => $value) { - $child->relation->$name = $value; + $child->query->$name = $value; } return $child; } - protected function buildTableAlias($element, &$count = 0) + /** + * @param JoinElement $element + */ + protected function buildTableAlias($element) { - if ($element->relation->tableAlias === null) { - $element->relation->tableAlias = 't' . ($count++); + if ($element->query->tableAlias !== null) { + $alias = $element->query->tableAlias; + } elseif ($element->query instanceof ActiveRelation) { + $alias = $element->query->name; + } else { + $alias = 't'; } + $count = 0; + while (isset($this->_tableAliases[$alias])) { + $alias = 't' . $count++; + } + $this->_tableAliases[$alias] = true; + $element->query->tableAlias = $alias; + foreach ($element->children as $child) { $this->buildTableAlias($child, $count); } } + + /** + * @param JoinElement $element + * @param Query $query + */ + protected function buildJoinQuery($element, $query) + { + $prefixes = array( + '@.' => $element->query->tableAlias . '.', + '?.' => $element->parent->query->tableAlias . '.', + ); + $quotedPrefixes = array( + '@.' => $this->connection->quoteTableName($element->query->tableAlias, true) . '.', + '?.' => $this->connection->quoteTableName($element->parent->query->tableAlias, true) . '.', + ); + + foreach ($this->buildSelect($element, $element->query->select) as $column) { + $query->select[] = strtr($column, $prefixes); + } + + if ($element->query->where !== null) { + $query->where[] = strtr($element->query->where, $quotedPrefixes); + } + + if ($element->query->having !== null) { + $query->having[] = strtr($element->query->having, $quotedPrefixes); + } + + if ($element->query->via !== null) { + $query->join[] = strtr($element->query->via, $quotedPrefixes); + } + + if ($element->query->joinType === null) { + $joinType = $element->query->select === false ? 'INNER JOIN' : 'LEFT JOIN'; + } else { + $joinType = $element->query->joinType; + } + $modelClass = $element->query->modelClass; + $tableName = $this->connection->quoteTableName($modelClass::tableName()); + $tableAlias = $this->connection->quoteTableName($element->query->tableAlias); + $join = "$joinType $tableName $tableAlias"; + if ($element->query->on !== null) { + $join .= ' ON ' . strtr($element->query->on, $quotedPrefixes); + } + $query->join[] = $join; + + if ($element->query->join !== null) { + $query->join[] = strtr($element->query->join, $quotedPrefixes); + } + + if ($element->query->orderBy !== null) { + if (!is_array($element->query->orderBy)) { + $element->query->orderBy = preg_split('/\s*,\s*/', trim($element->query->orderBy), -1, PREG_SPLIT_NO_EMPTY); + } + foreach ($element->query->orderBy as $orderBy) { + $query->orderBy[] = strtr($orderBy, $prefixes); + } + } + + if ($element->query->groupBy !== null) { + if (!is_array($element->query->groupBy)) { + $element->query->groupBy = preg_split('/\s*,\s*/', trim($element->query->groupBy), -1, PREG_SPLIT_NO_EMPTY); + } + foreach ($element->query->groupBy as $groupBy) { + $query->groupBy[] = strtr($groupBy, $prefixes); + } + } + + if ($element->query->params !== null) { + $query->addParams($element->query->params); + } + + foreach ($element->children as $child) { + $this->buildQuery($child, $query); + } + } + + protected function buildSelect($element, $select) + { + if ($select === false) { + return array(); + } + $modelClass = $element->query->modelClass; + $table = $modelClass::getMetaData()->table; + $columns = array(); + $columnCount = 0; + $prefix = $element->query->tableAlias; + if (empty($select) || $select === '*') { + foreach ($table->columns as $column) { + $alias = "t{$element->id}c" . ($columnCount++); + $columns[] = "$prefix.{$column->name} AS $alias"; + $element->columnAliases[$alias] = $column->name; + if ($column->isPrimaryKey) { + $element->pkAlias[$column->name] = $alias; + } + } + } else { + if (is_string($select)) { + $select = explode(',', $select); + } + foreach ($table->primaryKey as $column) { + $alias = "t{$element->id}c" . ($columnCount++); + $columns[] = "$prefix.$column AS $alias"; + $element->pkAlias[$column] = $alias; + } + foreach ($select as $column) { + $column = trim($column); + if (preg_match('/^(.*?)\s+AS\s+(\w+)$/im', $column, $matches)) { + // if the column is already aliased + $element->columnAliases[$matches[2]] = $matches[2]; + $columns[] = $column; + } elseif (!isset($element->pkAlias[$column])) { + $alias = "t{$element->id}c" . ($columnCount++); + $columns[] = "$prefix.$column AS $alias"; + $element->columnAliases[$alias] = $column; + } + } + } + + return $columns; + } } diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index ffb76be..13b8424 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -91,8 +91,8 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA public function one() { if ($this->records === null) { - // todo: load only one record - $this->records = $this->findRecords(false); + $this->limit = 1; + $this->records = $this->findRecords(); } return isset($this->records[0]) ? $this->records[0] : null; } @@ -240,13 +240,13 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA unset($this->records[$offset]); } - protected function findRecords($all = true) + protected function findRecords() { $finder = new ActiveFinder($this->getDbConnection()); if (!empty($this->with)) { - return $finder->findRecordsWithRelations(); + return $finder->findRecordsWithRelations($this); } else { - return $finder->findRecords($this, $all); + return $finder->findRecords($this); } } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index b665259..ce5e00f 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -517,10 +517,18 @@ abstract class ActiveRecord extends Model $this->_related[$relation->name] = $relation->hasMany ? array() : null; } + /** + * @param ActiveRelation $relation + * @param ActiveRecord $record + */ public function addRelatedRecord($relation, $record) { if ($relation->hasMany) { - $this->_related[$relation->name][] = $record; + if ($relation->indexBy !== null) { + $this->_related[$relation->name][$record->{$relation->indexBy}] = $record; + } else { + $this->_related[$relation->name][] = $record; + } } else { $this->_related[$relation->name] = $record; } @@ -1027,12 +1035,10 @@ abstract class ActiveRecord extends Model /** * Creates an active record with the given attributes. - * This method is internally used by the find methods. - * @param array $row attribute values (column name=>column value) - * @return ActiveRecord the newly created active record. The class of the object is the same as the model class. - * Null is returned if the input data is false. + * @param array $row attribute values (name => value) + * @return ActiveRecord the newly created active record. */ - public static function createRecord($row) + public static function create($row) { $record = static::instantiate($row); $columns = static::getMetaData()->table->columns; diff --git a/framework/db/ar/JoinElement.php b/framework/db/ar/JoinElement.php index 12ddb09..4a18875 100644 --- a/framework/db/ar/JoinElement.php +++ b/framework/db/ar/JoinElement.php @@ -18,9 +18,13 @@ use yii\db\Exception; class JoinElement extends \yii\base\Object { /** - * @var ActiveRelation + * @var integer ID of this join element */ - public $relation; + public $id; + /** + * @var BaseActiveQuery + */ + public $query; /** * @var JoinElement the parent element that this element needs to join with */ @@ -32,9 +36,9 @@ class JoinElement extends \yii\base\Object /** * @var JoinElement[] the child elements that have relations declared in the AR class of this element */ - public $relatedChildren = array(); + public $relations = array(); /** - * @var boolean whether this element is only for join purpose. If true, data will also be populated into the AR of this element. + * @var boolean whether this element is only for join purpose. If false, data will also be populated into the AR of this element. */ public $joinOnly; @@ -44,17 +48,27 @@ class JoinElement extends \yii\base\Object public $records; public $relatedRecords; - public function __construct($relation, $parent, $relatedParent) + /** + * @param ActiveRelation|ActiveQuery $query + * @param JoinElement $parent + * @param JoinElement $container + */ + public function __construct($id, $query, $parent, $container) { - $this->relation = $relation; + $this->id = $id; + $this->query = $query; if ($parent !== null) { $this->parent = $parent; - $parent->children[$relation->name] = $this; - $relatedParent->relatedChildren[$relation->name] = $this; + $parent->children[$query->name] = $this; + $container->relations[$query->name] = $this; } } - public function populateData($row) + /** + * @param array $row + * @return null|ActiveRecord + */ + public function createRecord($row) { $pk = array(); foreach ($this->pkAlias as $alias) { @@ -66,7 +80,7 @@ class JoinElement extends \yii\base\Object } $pk = count($pk) === 1 ? $pk[0] : serialize($pk); - // create active record + // create record if (isset($this->records[$pk])) { $record = $this->records[$pk]; } else { @@ -76,33 +90,37 @@ class JoinElement extends \yii\base\Object $attributes[$this->columnAliases[$alias]] = $value; } } - $modelClass = $this->relation->modelClass; - $record = $modelClass::populateData($attributes); + $modelClass = $this->query->modelClass; + $this->records[$pk] = $record = $modelClass::create($attributes); foreach ($this->children as $child) { - if ($child->relation->select !== false) { - $record->initRelation($child->relation); + if ($child->query->select !== false || $child->joinOnly) { + $record->initRelation($child->query); } } - $this->records[$pk] = $record; } - // populate child records - foreach ($this->relatedChildren as $child) { - if ($child->relation->select === false || $child->joinOnly) { + // add related records + foreach ($this->relations as $child) { + if ($child->query->select === false || $child->joinOnly) { continue; } - $childRecord = $child->populateData($row); + $childRecord = $child->createRecord($row); if ($childRecord === null) { continue; } - if ($child->relation->hasMany) { - $fpk = serialize($childRecord->getPrimaryKey()); - if (isset($this->relatedRecords[$pk][$child->relation->name][$fpk])) { - continue; + if ($child->query->hasMany) { + if ($child->query->indexBy !== null) { + $hash = $childRecord->{$child->query->indexBy}; + } else { + $hash = serialize($childRecord->getPrimaryKey()); + } + if (!isset($this->relatedRecords[$pk][$child->query->name][$hash])) { + $this->relatedRecords[$pk][$child->query->name][$hash] = true; + $record->addRelatedRecord($child->query, $childRecord); } - $this->relatedRecords[$pk][$child->relation->name][$fpk] = true; + } else { + $record->addRelatedRecord($child->query, $childRecord); } - $record->addRelatedRecord($child->relation, $childRecord); } return $record; @@ -110,52 +128,53 @@ class JoinElement extends \yii\base\Object public function buildQuery($query) { - $tokens = array( - '@.' => $this->relation->tableAlias . '.', - '?.' => $this->parent->relation->tableAlias . '.', + $prefixes = array( + '@.' => $this->query->tableAlias . '.', + '?.' => $this->parent->query->tableAlias . '.', ); - foreach ($this->buildSelect($this->relation->select) as $column) { - $query->select[] = strtr($column, $tokens); + $quotedPrefixes = ''; + foreach ($this->buildSelect($this->query->select) as $column) { + $query->select[] = strtr($column, $prefixes); } - if ($this->relation->where !== null) { - $query->where[] = strtr($this->relation->where, $tokens); + if ($this->query->where !== null) { + $query->where[] = strtr($this->query->where, $prefixes); } - if ($this->relation->having !== null) { - $query->having[] = strtr($this->relation->having, $tokens); + if ($this->query->having !== null) { + $query->having[] = strtr($this->query->having, $prefixes); } - if ($this->relation->via !== null) { - $query->join[] = $this->relation->via; + if ($this->query->via !== null) { + $query->join[] = $this->query->via; } - $modelClass = $this->relation->modelClass; + $modelClass = $this->query->modelClass; $tableName = $modelClass::tableName(); - $joinType = $this->relation->joinType === null ? 'LEFT JOIN' : $this->relation->joinType; - $join = "$joinType $tableName {$this->relation->tableAlias}"; - if ($this->relation->on !== null) { - $join .= ' ON ' . strtr($this->relation->on, $tokens); + $joinType = $this->query->joinType === null ? 'LEFT JOIN' : $this->query->joinType; + $join = "$joinType $tableName {$this->query->tableAlias}"; + if ($this->query->on !== null) { + $join .= ' ON ' . strtr($this->query->on, $prefixes); } $query->join[] = $join; - if ($this->relation->join !== null) { - $query->join[] = strtr($this->relation->join, $tokens); + if ($this->query->join !== null) { + $query->join[] = strtr($this->query->join, $prefixes); } // todo: convert orderBy to array first - if ($this->relation->orderBy !== null) { - $query->orderBy[] = strtr($this->relation->orderBy, $tokens); + if ($this->query->orderBy !== null) { + $query->orderBy[] = strtr($this->query->orderBy, $prefixes); } // todo: convert groupBy to array first - if ($this->relation->groupBy !== null) { - $query->groupBy[] = strtr($this->relation->groupBy, $tokens); + if ($this->query->groupBy !== null) { + $query->groupBy[] = strtr($this->query->groupBy, $prefixes); } - if ($this->relation->params !== null) { - foreach ($this->relation->params as $name => $value) { + if ($this->query->params !== null) { + foreach ($this->query->params as $name => $value) { if (is_integer($name)) { $query->params[] = $value; } else { @@ -171,14 +190,17 @@ class JoinElement extends \yii\base\Object public function buildSelect($select) { - $modelClass = $this->relation->modelClass; - $tableSchema = $modelClass::getMetaData()->table; + if ($select === false) { + return array(); + } + $modelClass = $this->query->modelClass; + $table = $modelClass::getMetaData()->table; $columns = array(); $columnCount = 0; - $prefix = $this->relation->tableAlias; + $prefix = $this->query->tableAlias; if (empty($select) || $select === '*') { - foreach ($tableSchema->columns as $column) { - $alias = $this->relation->tableAlias . '_' . ($columnCount++); + foreach ($table->columns as $column) { + $alias = "t{$this->id}c" . ($columnCount++); $columns[] = "$prefix.{$column->name} AS $alias"; $this->columnAliases[$alias] = $column->name; if ($column->isPrimaryKey) { @@ -189,8 +211,8 @@ class JoinElement extends \yii\base\Object if (is_string($select)) { $select = explode(',', $select); } - foreach ($tableSchema->primaryKey as $column) { - $alias = $this->relation->tableAlias . '_' . ($columnCount++); + foreach ($table->primaryKey as $column) { + $alias = "t{$this->id}c" . ($columnCount++); $columns[] = "$prefix.$column AS $alias"; $this->pkAlias[$column] = $alias; } @@ -201,7 +223,7 @@ class JoinElement extends \yii\base\Object $this->columnAliases[$matches[2]] = $matches[2]; $columns[] = $column; } elseif (!isset($this->pkAlias[$column])) { - $alias = $this->relation->tableAlias . '_' . ($columnCount++); + $alias = "t{$this->id}c" . ($columnCount++); $columns[] = "$prefix.$column AS $alias"; $this->columnAliases[$alias] = $column; }