diff --git a/framework/db/ar/ActiveFinder.php b/framework/db/ar/ActiveFinder.php index 163e842..93395f3 100644 --- a/framework/db/ar/ActiveFinder.php +++ b/framework/db/ar/ActiveFinder.php @@ -17,32 +17,6 @@ use yii\db\Exception; /** * ActiveFinder.php is ... - * todo: lazy loading - * todo: clean up joinOnly and select=false - * 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 * @@ -64,18 +38,18 @@ class ActiveFinder extends \yii\base\Object /** * @param ActiveQuery $query */ - public function findRecords($query) + public function find($query, $returnScalar = false) { if (!empty($query->with)) { - return $this->findRecordsWithRelations($query); + return $this->findWithRelations($query, $returnScalar); } if ($query->sql !== null) { $sql = $query->sql; } else { + $modelClass = $query->modelClass; + $tableName = $modelClass::tableName(); if ($query->from === null) { - $modelClass = $query->modelClass; - $tableName = $modelClass::tableName(); if ($query->tableAlias !== null) { $tableName .= ' ' . $query->tableAlias; } @@ -83,46 +57,88 @@ class ActiveFinder extends \yii\base\Object } $this->applyScopes($query); $sql = $this->connection->getQueryBuilder()->build($query); - $prefix = $this->connection->quoteTableName('@', true) . '.'; - if (strpos($sql, $prefix) !== false) { - if ($query->tableAlias !== null) { - $alias = $this->connection->quoteTableName($query->tableAlias) . '.'; - } else { - $class = $query->modelClass; - $alias = $this->connection->quoteTableName($class::tableName()) . '.'; - } - $sql = str_replace($prefix, $alias, $sql); + + if ($query->tableAlias !== null) { + $alias = $this->connection->quoteTableName($query->tableAlias) . '.'; + } else { + $alias = $this->connection->quoteTableName($tableName) . '.'; } + $tokens = array( + '@.' => $alias, + $this->connection->quoteTableName('@', true) . '.' => $alias, + ); + $sql = strtr($sql, $tokens); } $command = $this->connection->createCommand($sql, $query->params); - $rows = $command->queryAll(); - return $this->createRecords($query, $rows); + if ($returnScalar) { + return $command->queryScalar(); + } else { + $rows = $command->queryAll(); + return $this->createRecords($query, $rows); + } } - protected function createRecords($query, $rows) + private $_joinCount; + private $_tableAliases; + private $_hasMany; + + /** + * @param ActiveQuery $query + * @return array + */ + protected function findWithRelations($query, $returnScalar = false) { - $records = array(); - if ($query->asArray) { - if ($query->index === null) { - return $rows; + $this->_joinCount = 0; + $this->_tableAliases = array(); + $this->_hasMany = false; + $joinTree = new JoinElement($this->_joinCount++, $query, null, null); + + if ($query->sql !== null) { + $command = $this->connection->createCommand($query->sql, $query->params); + if ($returnScalar) { + return $command->queryScalar(); } - foreach ($rows as $row) { - $records[$row[$query->index]] = $row; + $rows = $command->queryAll(); + $records = $this->createRecords($query, $rows); + $modelClass = $query->modelClass; + $table = $modelClass::getMetaData()->table; + foreach ($records as $record) { + $pk = array(); + foreach ($table->primaryKey as $name) { + $pk[] = $record[$name]; + } + $pk = count($pk) === 1 ? $pk[0] : serialize($pk); + $joinTree->records[$pk] = $record; } + + $q = new ActiveQuery($modelClass); + $q->with = $query->with; + $q->tableAlias = 't'; + $q->asArray = $query->asArray; + $q->index = $query->index; + $q->select = $table->primaryKey; + $this->addPkCondition($q, $table, $rows, 't.'); + $joinTree->query = $query = $q; + } + + $this->buildJoinTree($joinTree, $query->with); + $this->initJoinTree($joinTree); + + $q = new Query; + $this->buildJoinQuery($joinTree, $q, $returnScalar); + + if ($returnScalar) { + return $q->createCommand($this->connection)->queryScalar(); } else { - $class = $query->modelClass; - if ($query->index === null) { - foreach ($rows as $row) { - $records[] = $class::create($row); - } - } else { - foreach ($rows as $row) { - $records[$row[$query->index]] = $class::create($row); - } + if ($this->_hasMany && ($query->limit > 0 || $query->offset > 0)) { + $this->limitQuery($query, $q); } + $command = $q->createCommand($this->connection); + $rows = $command->queryAll(); + $joinTree->populateData($rows); + return $query->index === null ? array_values($joinTree->records) : $joinTree->records; } - return $records; } /** @@ -130,7 +146,7 @@ class ActiveFinder extends \yii\base\Object * @param ActiveRelation $relation * @return array */ - public function findRelatedRecords($record, $relation) + public function findWithRecord($record, $relation) { $this->_joinCount = 0; $this->_tableAliases = array(); @@ -169,69 +185,33 @@ class ActiveFinder extends \yii\base\Object } } - private $_joinCount; - private $_tableAliases; - private $_hasMany; - - /** - * @param ActiveQuery $query - * @return array - */ - public function findRecordsWithRelations($query) + protected function createRecords($query, $rows) { - if ($query->sql !== null) { - $command = $this->connection->createCommand($query->sql, $query->params); - $rows = $command->queryAll(); - $records = $this->createRecords($query, $rows); - $q = new ActiveQuery($query->modelClass); - $q->with = $query->with; - $q->tableAlias = 't'; - $q->asArray = $query->asArray; - $q->index = $query->index; - $modelClass = $query->modelClass; - $table = $modelClass::getMetaData()->table; - $q->select = $table->primaryKey; - $this->addPkCondition($q, $table, $rows, 't.'); - $query = $q; - } - - $this->_joinCount = 0; - $this->_tableAliases = array(); - $this->_hasMany = false; - $joinTree = new JoinElement($this->_joinCount++, $query, null, null); - - if (isset($records)) { - foreach ($records as $record) { - $pk = array(); - foreach ($table->primaryKey as $name) { - $pk[] = $record[$name]; + $records = array(); + if ($query->asArray) { + if ($query->index === null) { + return $rows; + } + foreach ($rows as $row) { + $records[$row[$query->index]] = $row; + } + } else { + $class = $query->modelClass; + if ($query->index === null) { + foreach ($rows as $row) { + $records[] = $class::create($row); + } + } else { + foreach ($rows as $row) { + $records[$row[$query->index]] = $class::create($row); } - $pk = count($pk) === 1 ? $pk[0] : serialize($pk); - $joinTree->records[$pk] = $record; } } - - $this->buildJoinTree($joinTree, $query->with); - $this->initJoinTree($joinTree, !isset($records)); - - $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); - - return $query->index === null ? array_values($joinTree->records) : $joinTree->records; + return $records; } protected function applyScopes($query) { - if ($query->modelClass === null || $query instanceof ActiveQuery && $query->sql !== null) { - return; - } $class = $query->modelClass; $class::defaultScope($query); if (is_array($query->scopes)) { @@ -281,7 +261,6 @@ class ActiveFinder extends \yii\base\Object if (isset($parent->children[$with])) { $child = $parent->children[$with]; - $child->joinOnly = false; } else { $modelClass = $parent->query->modelClass; $relations = $modelClass::getMetaData()->relations; @@ -292,8 +271,9 @@ class ActiveFinder extends \yii\base\Object if (is_string($relation->via)) { // join via an existing relation $parent2 = $this->buildJoinTree($parent, $relation->via); - if ($parent2->joinOnly === null) { - $parent2->joinOnly = true; + if ($parent2->query->select === null) { + $parent2->query->select = false; + unset($parent2->container->relations[$parent2->query->name]); } $child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent); } elseif (is_array($relation->via)) { @@ -312,7 +292,6 @@ class ActiveFinder extends \yii\base\Object } $parent2 = new JoinElement($this->_joinCount++, $r, $parent, $parent); - $parent2->joinOnly = true; $child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent); } else { @@ -329,9 +308,8 @@ class ActiveFinder extends \yii\base\Object /** * @param JoinElement $element - * @param boolean $applyScopes */ - protected function initJoinTree($element, $applyScopes = true) + protected function initJoinTree($element) { if ($element->query->tableAlias !== null) { $alias = $element->query->tableAlias; @@ -355,7 +333,7 @@ class ActiveFinder extends \yii\base\Object $this->_tableAliases[$alias] = true; $element->query->tableAlias = $alias; - if ($applyScopes) { + if ($element->records !== array()) { $this->applyScopes($element->query); } @@ -364,7 +342,7 @@ class ActiveFinder extends \yii\base\Object } foreach ($element->children as $child) { - $this->initJoinTree($child, $count); + $this->initJoinTree($child); } } @@ -372,7 +350,7 @@ class ActiveFinder extends \yii\base\Object * @param JoinElement $element * @param \yii\db\dao\Query $query */ - protected function buildJoinQuery($element, $query) + protected function buildJoinQuery($element, $query, $keepSelect = false) { if ($element->parent) { $prefixes = array( @@ -396,8 +374,20 @@ class ActiveFinder extends \yii\base\Object $qb = $this->connection->getQueryBuilder(); - foreach ($this->buildSelect($element, $element->query->select) as $column) { - $query->select[] = strtr($column, $prefixes); + if ($keepSelect) { + if (!empty($element->query->select)) { + $select = $element->query->select; + if (is_string($select)) { + $select = explode(',', $select); + } + foreach ($select as $column) { + $query->select[] = strtr(trim($column), $prefixes); + } + } + } else { + foreach ($this->buildSelect($element, $element->query->select) as $column) { + $query->select[] = strtr($column, $prefixes); + } } if ($element->query instanceof ActiveQuery) { @@ -492,7 +482,7 @@ class ActiveFinder extends \yii\base\Object } foreach ($element->children as $child) { - $this->buildJoinQuery($child, $query); + $this->buildJoinQuery($child, $query, $keepSelect); } } @@ -534,7 +524,11 @@ class ActiveFinder extends \yii\base\Object $columns[] = $column; } elseif (!isset($element->pkAlias[$column])) { $alias = "c{$element->id}_" . ($columnCount++); - $columns[] = "$prefix.$column AS $alias"; + if (strpos($column, '(') !== false) { + $columns[] = "$column AS $alias"; + } else { + $columns[] = "$prefix.$column AS $alias"; + } $element->columnAliases[$alias] = $column; } } diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index 3072ac5..6403b83 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -18,20 +18,6 @@ use yii\db\Exception; * 1. eager loading, base limited and has has_many relations * 2. * ActiveFinder.php is ... - * todo: add SQL monitor - * todo: better handling on join() support in QueryBuilder: use regexp to detect table name and quote it - * todo: do not support anonymous parameter binding - * todo: quote join/on part of the relational query - * todo: modify QueryBuilder about join() methods - * todo: unify ActiveFinder and ActiveRelation in query building process - * todo: intelligent table aliasing (first table name, then relation name, finally t?) - * todo: allow using tokens in primary query fragments - * todo: findBySql - * todo: base limited - * todo: lazy loading - * todo: scope - * todo: test via option - * todo: count, sum, exists * * @property integer $count * @@ -97,15 +83,21 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA return isset($this->records[0]) ? $this->records[0] : null; } + /** + * Returns a scalar value for this query. + * The value returned will be the first column in the first row of the query results. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if there is no value. + */ public function value() { - $result = $this->asArray()->one(); - return $result === null ? null : reset($result); + $finder = new ActiveFinder($this->getDbConnection()); + return $finder->find($this, true); } public function exists() { - return $this->select(array(new Expression('1')))->asArray()->one() !== null; + return $this->select(array(new Expression('1')))->value() !== false; } /** @@ -243,6 +235,6 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA protected function findRecords() { $finder = new ActiveFinder($this->getDbConnection()); - return $finder->findRecords($this); + return $finder->find($this); } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 751d816..72a0fa7 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -134,13 +134,17 @@ abstract class ActiveRecord extends Model * * ~~~ * // count the total number of customers - * echo Customer::count(); - * // count the number of customers whose primary key value is 10. - * echo Customer::count(10); + * echo Customer::count()->value(); * // count the number of active customers: * echo Customer::count(array( * 'where' => array('status' => 1), - * )); + * ))->value(); + * // equivalent usage: + * echo Customer::count() + * ->where(array('status' => 1)) + * ->value(); + * // customize the count option + * echo Customer::count('COUNT(DISTINCT age)')->value(); * ~~~ * * @param mixed $q the query parameter. This can be one of the followings: @@ -157,13 +161,9 @@ abstract class ActiveRecord extends Model foreach ($q as $name => $value) { $query->$name = $value; } - } elseif ($q !== null) { - // query by primary key - $primaryKey = static::getMetaData()->table->primaryKey; - $query->where(array($primaryKey[0] => $q)); } if ($query->select === null) { - $query->select = 'COUNT(*)'; + $query->select = array('COUNT(*)'); } return $query->value(); } @@ -561,7 +561,7 @@ abstract class ActiveRecord extends Model } $finder = new ActiveFinder($this->getDbConnection()); - return $finder->findRelatedRecords($this, $relation); + return $finder->findWithRecord($this, $relation); } /** diff --git a/framework/db/ar/JoinElement.php b/framework/db/ar/JoinElement.php index 92a8194..65f80c1 100644 --- a/framework/db/ar/JoinElement.php +++ b/framework/db/ar/JoinElement.php @@ -39,10 +39,6 @@ class JoinElement extends \yii\base\Object */ public $relations = array(); /** - * @var boolean whether this element is only for join purpose. If false, data will be populated into the AR of this element. - */ - public $joinOnly; - /** * @var array column aliases (alias => original name) */ public $columnAliases = array(); diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index 89e38dd..c4c448b 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -225,18 +225,21 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase public function testEagerLoading() { + // has many $customers = Customer::find()->with('orders')->order('@.id')->all(); $this->assertEquals(3, count($customers)); $this->assertEquals(1, count($customers[0]->orders)); $this->assertEquals(2, count($customers[1]->orders)); $this->assertEquals(0, count($customers[2]->orders)); + // nested $customers = Customer::find()->with('orders.customer')->order('@.id')->all(); $this->assertEquals(3, count($customers)); $this->assertEquals(1, $customers[0]->orders[0]->customer->id); $this->assertEquals(2, $customers[1]->orders[0]->customer->id); $this->assertEquals(2, $customers[1]->orders[1]->customer->id); + // has many via relation $orders = Order::find()->with('items')->order('@.id')->all(); $this->assertEquals(3, count($orders)); $this->assertEquals(1, $orders[0]->items[0]->id); @@ -245,18 +248,22 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals(4, $orders[1]->items[1]->id); $this->assertEquals(5, $orders[1]->items[2]->id); + // has many via join table $orders = Order::find()->with('books')->order('@.id')->all(); $this->assertEquals(2, count($orders)); $this->assertEquals(1, $orders[0]->books[0]->id); $this->assertEquals(2, $orders[0]->books[1]->id); $this->assertEquals(2, $orders[1]->books[0]->id); + // has many and base limited $orders = Order::find()->with('items')->order('@.id')->limit(2)->all(); $this->assertEquals(2, count($orders)); + // findBySql with $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); $this->assertEquals(2, count($orders)); + // index and array $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])); @@ -265,6 +272,15 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals(2, count($customers[2]['orders'])); $this->assertEquals(0, count($customers[3]['orders'])); $this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); + + // count with + $this->assertEquals(3, Order::count()); + $value = Order::count(array( + 'select' => array('COUNT(DISTINCT @.id, @.customer_id)'), + 'with' => 'books', + )); + $this->assertEquals(2, $value); + } public function testLazyLoading()