diff --git a/framework/base/Object.php b/framework/base/Object.php index 3d9f7a7..44b3308 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -216,7 +216,7 @@ class Object */ public function canSetProperty($name, $checkVar = true) { - return method_exists($this, 'set' . $name) || $checkVar && property_exists($this, $name); + return $checkVar && property_exists($this, $name) || method_exists($this, 'set' . $name); } /** diff --git a/framework/db/ar/ActiveFinder.php b/framework/db/ar/ActiveFinder.php index 1db8f2d..3e739b9 100644 --- a/framework/db/ar/ActiveFinder.php +++ b/framework/db/ar/ActiveFinder.php @@ -21,6 +21,30 @@ use yii\db\Exception; * todo: clean up joinOnly and select=false * todo: records for index != null, asArray = true * todo: refactor code + * todo: count with + * todo: findBySql and lazy loading cannot apply scopes for primary table + * + * Four cases: + * 1. normal eager loading + * 2. eager loading, base limited and has many + * 3. findBySql and eager loading + * 4. lazy loading + * + * Build a join tree + * Update join tree + * Case 2: + * Find PKs for primary table + * Modify main query with the found PK, reset limit/offset + * Case 3: + * Find records by SQL + * Reset main query and set WHERE with the found PK + * Set root.records = the found records + * Case 4: + * Set root.records = the primary record + * Generate join query + * Case 4: + * If + * * @property integer $count * * @author Qiang Xue @@ -101,10 +125,38 @@ class ActiveFinder extends \yii\base\Object return $records; } - - public function findRelatedRecords($record, $relation, $params) + public function findRelatedRecords($record, $relation) { + $this->_joinCount = 0; + $this->_tableAliases = array(); + $this->_hasMany = false; + $query = new ActiveQuery(get_class($record)); + $joinTree = new JoinElement($this->_joinCount++, $query, null, null); + $child = $this->buildJoinTree($joinTree, $relation->name); + $this->buildJoinTree($child, $relation->with); + $this->initJoinTree($joinTree); + // todo: set where by pk + + $q = new Query; + $this->buildJoinQuery($joinTree, $q); + + if ($this->_hasMany && ($query->limit > 0 || $query->offset > 0)) { + $this->limitQuery($query, $q); + } + + $rows = $q->createCommand($this->connection)->queryAll(); + $joinTree->populateData($rows); + + if ($query->index !== null) { + $records = array(); + foreach ($joinTree->records as $record) { + $records[$record[$query->index]] = $record; + } + return $records; + } else { + return array_values($joinTree->records); + } } private $_joinCount; @@ -150,7 +202,7 @@ class ActiveFinder extends \yii\base\Object } $this->buildJoinTree($joinTree, $query->with); - $this->initJoinTree($joinTree); + $this->initJoinTree($joinTree, !isset($records)); $q = new Query; $this->buildJoinQuery($joinTree, $q); @@ -160,19 +212,9 @@ class ActiveFinder extends \yii\base\Object } $rows = $q->createCommand($this->connection)->queryAll(); - foreach ($rows as $row) { - $joinTree->createRecord($row); - } + $joinTree->populateData($rows); - if ($query->index !== null) { - $records = array(); - foreach ($joinTree->records as $record) { - $records[$record[$query->index]] = $record; - } - return $records; - } else { - return array_values($joinTree->records); - } + return $query->index === null ? array_values($joinTree->records) : $joinTree->records; } protected function applyScopes($query) @@ -208,6 +250,9 @@ class ActiveFinder extends \yii\base\Object */ protected function buildJoinTree($parent, $with, $config = array()) { + if (empty($with)) { + return null; + } if (is_array($with)) { foreach ($with as $name => $value) { if (is_array($value)) { @@ -274,8 +319,9 @@ class ActiveFinder extends \yii\base\Object /** * @param JoinElement $element + * @param boolean $applyScopes */ - protected function initJoinTree($element) + protected function initJoinTree($element, $applyScopes = true) { if ($element->query->tableAlias !== null) { $alias = $element->query->tableAlias; @@ -299,7 +345,13 @@ class ActiveFinder extends \yii\base\Object $this->_tableAliases[$alias] = true; $element->query->tableAlias = $alias; - $this->applyScopes($element->query); + if ($applyScopes) { + $this->applyScopes($element->query); + } + + if ($element->container !== null && $element->query->asArray === null) { + $element->query->asArray = $element->container->query->asArray; + } foreach ($element->children as $child) { $this->initJoinTree($child, $count); @@ -444,24 +496,26 @@ class ActiveFinder extends \yii\base\Object $columns = array(); $columnCount = 0; $prefix = $element->query->tableAlias; + + foreach ($table->primaryKey as $column) { + $alias = "c{$element->id}_" . ($columnCount++); + $columns[] = "$prefix.$column AS $alias"; + $element->pkAlias[$column] = $alias; + $element->columnAliases[$alias] = $column; + } + if (empty($select) || $select === '*') { foreach ($table->columns as $column) { - $alias = "c{$element->id}_" . ($columnCount++); - $columns[] = "$prefix.{$column->name} AS $alias"; - $element->columnAliases[$alias] = $column->name; - if ($column->isPrimaryKey) { - $element->pkAlias[$column->name] = $alias; + if (!isset($element->pkAlias[$column->name])) { + $alias = "c{$element->id}_" . ($columnCount++); + $columns[] = "$prefix.{$column->name} AS $alias"; + $element->columnAliases[$alias] = $column->name; } } } else { if (is_string($select)) { $select = explode(',', $select); } - foreach ($table->primaryKey as $column) { - $alias = "c{$element->id}_" . ($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)) { @@ -476,6 +530,18 @@ class ActiveFinder extends \yii\base\Object } } + // determine the actual index column(s) + if ($element->query->index !== null) { + $index = array_search($element->query->index, $element->columnAliases); + } + if (empty($index)) { + $index = $element->pkAlias; + if (count($index) === 1) { + $index = reset($element->pkAlias); + } + } + $element->key = $index; + return $columns; } @@ -506,7 +572,6 @@ class ActiveFinder extends \yii\base\Object $query->andWhere(array('in', $prefix . $name, $values)); } else { $ors = array('or'); - $prefix = $this->connection->quoteTableName($activeQuery->tableAlias, true) . '.'; foreach ($rows as $row) { $hash = array(); foreach ($table->primaryKey as $name) { diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index cb31f8d..0a0e375 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -555,8 +555,12 @@ abstract class ActiveRecord extends Model } $relation = $md->relations[$relation]; } - $query = $this->createActiveQuery(); - return $query->findRelatedRecords($this, $relation, $params); + foreach ($params as $name => $value) { + $relation->$name = $value; + } + + $finder = new ActiveFinder($this->getDbConnection()); + return $finder->findRelatedRecords($this, $relation); } /** @@ -1045,7 +1049,7 @@ abstract class ActiveRecord extends Model foreach ($row as $name => $value) { if (isset($columns[$name])) { $record->_attributes[$name] = $value; - } elseif ($record->canSetProperty($name)) { + } else { $record->$name = $value; } } diff --git a/framework/db/ar/JoinElement.php b/framework/db/ar/JoinElement.php index 51605d8..9d4bf3d 100644 --- a/framework/db/ar/JoinElement.php +++ b/framework/db/ar/JoinElement.php @@ -29,24 +29,36 @@ class JoinElement extends \yii\base\Object * @var JoinElement the parent element that this element needs to join with */ public $parent; + public $container; /** * @var JoinElement[] the child elements that need to join with this element */ public $children = array(); /** - * @var JoinElement[] the child elements that have relations declared in the AR class of this element + * @var JoinElement[] the child elements that have corresponding relations declared in the AR class of this element */ public $relations = array(); /** - * @var boolean whether this element is only for join purpose. If false, 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 be populated into the AR of this element. */ public $joinOnly; - - public $columnAliases = array(); // alias => original name - public $pkAlias = array(); // original name => alias - + /** + * @var array column aliases (alias => original name) + */ + public $columnAliases = array(); + /** + * @var array primary key column aliases (original name => alias) + */ + public $pkAlias = array(); + /** + * @var string|array the column(s) used for index the query results + */ + public $key; + /** + * @var array query results for this element (PK value => AR instance or data array) + */ public $records; - public $relatedRecords; + public $related; /** * @param integer $id @@ -60,71 +72,106 @@ class JoinElement extends \yii\base\Object $this->query = $query; if ($parent !== null) { $this->parent = $parent; + $this->container = $container; $parent->children[$query->name] = $this; - $container->relations[$query->name] = $this; + if ($query->select !== false) { + $container->relations[$query->name] = $this; + } } } - /** - * @param array $row - * @return null|ActiveRecord - */ - public function createRecord($row) + public function populateData($rows) { - $pk = array(); - foreach ($this->pkAlias as $alias) { - if (isset($row[$alias])) { - $pk[] = $row[$alias]; - } else { - return null; + if ($this->container === null) { + foreach ($rows as $row) { + if (($key = $this->getKeyValue($row)) !== null && !isset($this->records[$key])) { + $this->records[$key] = $this->createRecord($row); + } } - } - $pk = count($pk) === 1 ? $pk[0] : serialize($pk); - - // create record - // todo: asArray - if (isset($this->records[$pk])) { - $record = $this->records[$pk]; } else { - $attributes = array(); - foreach ($row as $alias => $value) { - if (isset($this->columnAliases[$alias])) { - $attributes[$this->columnAliases[$alias]] = $value; + foreach ($rows as $row) { + $key = $this->getKeyValue($row); + $containerKey = $this->container->getKeyValue($row); + if ($key === null || $containerKey === null || isset($this->related[$containerKey][$key])) { + continue; } - } - $modelClass = $this->query->modelClass; - $this->records[$pk] = $record = $modelClass::create($attributes); - foreach ($this->children as $child) { - if ($child->query->select !== false && !$child->joinOnly) { - $record->initRelation($child->query); + $this->related[$containerKey][$key] = true; + if ($this->query->asArray) { + if (isset($this->records[$key])) { + if ($this->query->hasMany) { + if ($this->query->index !== null) { + $this->container->records[$containerKey][$this->query->name][$key] =& $this->records[$key]; + } else { + $this->container->records[$containerKey][$this->query->name][] =& $this->records[$key]; + } + } else { + $this->container->records[$containerKey][$this->query->name] =& $this->records[$key]; + } + } else { + $record = $this->createRecord($row); + if ($this->query->hasMany) { + if ($this->query->index !== null) { + $this->container->records[$containerKey][$this->query->name][$key] = $record; + $this->records[$key] =& $this->container->records[$containerKey][$this->query->name][$key]; + } else { + $count = count($this->container->records[$containerKey][$this->query->name]); + $this->container->records[$containerKey][$this->query->name][] = $record; + $this->records[$key] =& $this->container->records[$containerKey][$this->query->name][$count]; + } + } else { + $this->container->records[$containerKey][$this->query->name] = $record; + $this->records[$key] =& $this->container->records[$containerKey][$this->query->name]; + } + } + } else { + if (isset($this->records[$key])) { + $record = $this->records[$key]; + } else { + $this->records[$key] = $record = $this->createRecord($row); + } + $this->container->records[$containerKey]->addRelatedRecord($this->query, $record); } } } - // add related records foreach ($this->relations as $child) { - if ($child->query->select === false || $child->joinOnly) { - continue; - } - $childRecord = $child->createRecord($row); - if ($childRecord === null) { - continue; - } - if ($child->query->hasMany) { - if ($child->query->index !== null) { - $hash = $childRecord[$child->query->index]; - } 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); + $child->populateData($rows); + } + } + + protected function getKeyValue($row) + { + if (is_array($this->key)) { + $key = array(); + foreach ($this->key as $alias) { + if (!isset($row[$alias])) { + return null; } - } else { - $record->addRelatedRecord($child->query, $childRecord); + $key[] = $row[$alias]; } + return serialize($key); + } else { + return $row[$this->key]; } + } + protected function createRecord($row) + { + $record = array(); + foreach ($this->columnAliases as $alias => $name) { + $record[$name] = $row[$alias]; + } + if ($this->query->asArray) { + foreach ($this->relations as $child) { + $record[$child->query->name] = $child->query->hasMany ? array() : null; + } + } else { + $modelClass = $this->query->modelClass; + $record = $modelClass::create($record); + foreach ($this->relations as $child) { + $record->{$child->query->name} = $child->query->hasMany ? array() : null; + } + } return $record; } } \ No newline at end of file diff --git a/framework/db/dao/Command.php b/framework/db/dao/Command.php index 8baa7b0..6df40a9 100644 --- a/framework/db/dao/Command.php +++ b/framework/db/dao/Command.php @@ -222,7 +222,7 @@ class Command extends \yii\base\Component } \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); - +echo $sql . "\n\n"; try { if ($this->connection->enableProfiling) { \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index f483513..bd8e7f0 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -256,6 +256,15 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); $this->assertEquals(2, count($orders)); + + $customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue(isset($customers[1], $customers[2], $customers[3])); + $this->assertTrue(is_array($customers[1])); + $this->assertEquals(1, count($customers[1]['orders'])); + $this->assertEquals(2, count($customers[2]['orders'])); + $this->assertEquals(0, count($customers[3]['orders'])); + $this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); } /*