Commit ed513b4d by Qiang Xue

...

parent 5ddc1ba1
......@@ -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;
}
}
......
......@@ -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);
}
}
......@@ -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);
}
/**
......
......@@ -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();
......
......@@ -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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment