Browse Source

...

tags/2.0.0-beta
Qiang Xue 13 years ago
parent
commit
ed513b4da9
  1. 248
      framework/db/ar/ActiveFinder.php
  2. 28
      framework/db/ar/ActiveQuery.php
  3. 20
      framework/db/ar/ActiveRecord.php
  4. 4
      framework/db/ar/JoinElement.php
  5. 16
      tests/unit/framework/db/ar/ActiveRecordTest.php

248
framework/db/ar/ActiveFinder.php

@ -17,32 +17,6 @@ use yii\db\Exception;
/** /**
* ActiveFinder.php is ... * 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 * @property integer $count
* *
@ -64,18 +38,18 @@ class ActiveFinder extends \yii\base\Object
/** /**
* @param ActiveQuery $query * @param ActiveQuery $query
*/ */
public function findRecords($query) public function find($query, $returnScalar = false)
{ {
if (!empty($query->with)) { if (!empty($query->with)) {
return $this->findRecordsWithRelations($query); return $this->findWithRelations($query, $returnScalar);
} }
if ($query->sql !== null) { if ($query->sql !== null) {
$sql = $query->sql; $sql = $query->sql;
} else { } else {
$modelClass = $query->modelClass;
$tableName = $modelClass::tableName();
if ($query->from === null) { if ($query->from === null) {
$modelClass = $query->modelClass;
$tableName = $modelClass::tableName();
if ($query->tableAlias !== null) { if ($query->tableAlias !== null) {
$tableName .= ' ' . $query->tableAlias; $tableName .= ' ' . $query->tableAlias;
} }
@ -83,46 +57,88 @@ class ActiveFinder extends \yii\base\Object
} }
$this->applyScopes($query); $this->applyScopes($query);
$sql = $this->connection->getQueryBuilder()->build($query); $sql = $this->connection->getQueryBuilder()->build($query);
$prefix = $this->connection->quoteTableName('@', true) . '.';
if (strpos($sql, $prefix) !== false) { if ($query->tableAlias !== null) {
if ($query->tableAlias !== null) { $alias = $this->connection->quoteTableName($query->tableAlias) . '.';
$alias = $this->connection->quoteTableName($query->tableAlias) . '.'; } else {
} else { $alias = $this->connection->quoteTableName($tableName) . '.';
$class = $query->modelClass;
$alias = $this->connection->quoteTableName($class::tableName()) . '.';
}
$sql = str_replace($prefix, $alias, $sql);
} }
$tokens = array(
'@.' => $alias,
$this->connection->quoteTableName('@', true) . '.' => $alias,
);
$sql = strtr($sql, $tokens);
} }
$command = $this->connection->createCommand($sql, $query->params); $command = $this->connection->createCommand($sql, $query->params);
$rows = $command->queryAll(); if ($returnScalar) {
return $this->createRecords($query, $rows); 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(); $this->_joinCount = 0;
if ($query->asArray) { $this->_tableAliases = array();
if ($query->index === null) { $this->_hasMany = false;
return $rows; $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) { $rows = $command->queryAll();
$records[$row[$query->index]] = $row; $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 { } else {
$class = $query->modelClass; if ($this->_hasMany && ($query->limit > 0 || $query->offset > 0)) {
if ($query->index === null) { $this->limitQuery($query, $q);
foreach ($rows as $row) {
$records[] = $class::create($row);
}
} else {
foreach ($rows as $row) {
$records[$row[$query->index]] = $class::create($row);
}
} }
$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 * @param ActiveRelation $relation
* @return array * @return array
*/ */
public function findRelatedRecords($record, $relation) public function findWithRecord($record, $relation)
{ {
$this->_joinCount = 0; $this->_joinCount = 0;
$this->_tableAliases = array(); $this->_tableAliases = array();
@ -169,69 +185,33 @@ class ActiveFinder extends \yii\base\Object
} }
} }
private $_joinCount; protected function createRecords($query, $rows)
private $_tableAliases;
private $_hasMany;
/**
* @param ActiveQuery $query
* @return array
*/
public function findRecordsWithRelations($query)
{ {
if ($query->sql !== null) { $records = array();
$command = $this->connection->createCommand($query->sql, $query->params); if ($query->asArray) {
$rows = $command->queryAll(); if ($query->index === null) {
$records = $this->createRecords($query, $rows); return $rows;
$q = new ActiveQuery($query->modelClass); }
$q->with = $query->with; foreach ($rows as $row) {
$q->tableAlias = 't'; $records[$row[$query->index]] = $row;
$q->asArray = $query->asArray; }
$q->index = $query->index; } else {
$modelClass = $query->modelClass; $class = $query->modelClass;
$table = $modelClass::getMetaData()->table; if ($query->index === null) {
$q->select = $table->primaryKey; foreach ($rows as $row) {
$this->addPkCondition($q, $table, $rows, 't.'); $records[] = $class::create($row);
$query = $q; }
} } else {
foreach ($rows as $row) {
$this->_joinCount = 0; $records[$row[$query->index]] = $class::create($row);
$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];
} }
$pk = count($pk) === 1 ? $pk[0] : serialize($pk);
$joinTree->records[$pk] = $record;
} }
} }
return $records;
$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;
} }
protected function applyScopes($query) protected function applyScopes($query)
{ {
if ($query->modelClass === null || $query instanceof ActiveQuery && $query->sql !== null) {
return;
}
$class = $query->modelClass; $class = $query->modelClass;
$class::defaultScope($query); $class::defaultScope($query);
if (is_array($query->scopes)) { if (is_array($query->scopes)) {
@ -281,7 +261,6 @@ class ActiveFinder extends \yii\base\Object
if (isset($parent->children[$with])) { if (isset($parent->children[$with])) {
$child = $parent->children[$with]; $child = $parent->children[$with];
$child->joinOnly = false;
} else { } else {
$modelClass = $parent->query->modelClass; $modelClass = $parent->query->modelClass;
$relations = $modelClass::getMetaData()->relations; $relations = $modelClass::getMetaData()->relations;
@ -292,8 +271,9 @@ class ActiveFinder extends \yii\base\Object
if (is_string($relation->via)) { if (is_string($relation->via)) {
// join via an existing relation // join via an existing relation
$parent2 = $this->buildJoinTree($parent, $relation->via); $parent2 = $this->buildJoinTree($parent, $relation->via);
if ($parent2->joinOnly === null) { if ($parent2->query->select === null) {
$parent2->joinOnly = true; $parent2->query->select = false;
unset($parent2->container->relations[$parent2->query->name]);
} }
$child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent); $child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent);
} elseif (is_array($relation->via)) { } elseif (is_array($relation->via)) {
@ -312,7 +292,6 @@ class ActiveFinder extends \yii\base\Object
} }
$parent2 = new JoinElement($this->_joinCount++, $r, $parent, $parent); $parent2 = new JoinElement($this->_joinCount++, $r, $parent, $parent);
$parent2->joinOnly = true;
$child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent); $child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent);
} else { } else {
@ -329,9 +308,8 @@ class ActiveFinder extends \yii\base\Object
/** /**
* @param JoinElement $element * @param JoinElement $element
* @param boolean $applyScopes
*/ */
protected function initJoinTree($element, $applyScopes = true) protected function initJoinTree($element)
{ {
if ($element->query->tableAlias !== null) { if ($element->query->tableAlias !== null) {
$alias = $element->query->tableAlias; $alias = $element->query->tableAlias;
@ -355,7 +333,7 @@ class ActiveFinder extends \yii\base\Object
$this->_tableAliases[$alias] = true; $this->_tableAliases[$alias] = true;
$element->query->tableAlias = $alias; $element->query->tableAlias = $alias;
if ($applyScopes) { if ($element->records !== array()) {
$this->applyScopes($element->query); $this->applyScopes($element->query);
} }
@ -364,7 +342,7 @@ class ActiveFinder extends \yii\base\Object
} }
foreach ($element->children as $child) { 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 JoinElement $element
* @param \yii\db\dao\Query $query * @param \yii\db\dao\Query $query
*/ */
protected function buildJoinQuery($element, $query) protected function buildJoinQuery($element, $query, $keepSelect = false)
{ {
if ($element->parent) { if ($element->parent) {
$prefixes = array( $prefixes = array(
@ -396,8 +374,20 @@ class ActiveFinder extends \yii\base\Object
$qb = $this->connection->getQueryBuilder(); $qb = $this->connection->getQueryBuilder();
foreach ($this->buildSelect($element, $element->query->select) as $column) { if ($keepSelect) {
$query->select[] = strtr($column, $prefixes); 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) { if ($element->query instanceof ActiveQuery) {
@ -492,7 +482,7 @@ class ActiveFinder extends \yii\base\Object
} }
foreach ($element->children as $child) { 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; $columns[] = $column;
} elseif (!isset($element->pkAlias[$column])) { } elseif (!isset($element->pkAlias[$column])) {
$alias = "c{$element->id}_" . ($columnCount++); $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; $element->columnAliases[$alias] = $column;
} }
} }

28
framework/db/ar/ActiveQuery.php

@ -18,20 +18,6 @@ use yii\db\Exception;
* 1. eager loading, base limited and has has_many relations * 1. eager loading, base limited and has has_many relations
* 2. * 2.
* ActiveFinder.php is ... * 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 * @property integer $count
* *
@ -97,15 +83,21 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA
return isset($this->records[0]) ? $this->records[0] : null; 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() public function value()
{ {
$result = $this->asArray()->one(); $finder = new ActiveFinder($this->getDbConnection());
return $result === null ? null : reset($result); return $finder->find($this, true);
} }
public function exists() 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() protected function findRecords()
{ {
$finder = new ActiveFinder($this->getDbConnection()); $finder = new ActiveFinder($this->getDbConnection());
return $finder->findRecords($this); return $finder->find($this);
} }
} }

20
framework/db/ar/ActiveRecord.php

@ -134,13 +134,17 @@ abstract class ActiveRecord extends Model
* *
* ~~~ * ~~~
* // count the total number of customers * // count the total number of customers
* echo Customer::count(); * echo Customer::count()->value();
* // count the number of customers whose primary key value is 10.
* echo Customer::count(10);
* // count the number of active customers: * // count the number of active customers:
* echo Customer::count(array( * echo Customer::count(array(
* 'where' => array('status' => 1), * '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: * @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) { foreach ($q as $name => $value) {
$query->$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) { if ($query->select === null) {
$query->select = 'COUNT(*)'; $query->select = array('COUNT(*)');
} }
return $query->value(); return $query->value();
} }
@ -561,7 +561,7 @@ abstract class ActiveRecord extends Model
} }
$finder = new ActiveFinder($this->getDbConnection()); $finder = new ActiveFinder($this->getDbConnection());
return $finder->findRelatedRecords($this, $relation); return $finder->findWithRecord($this, $relation);
} }
/** /**

4
framework/db/ar/JoinElement.php

@ -39,10 +39,6 @@ class JoinElement extends \yii\base\Object
*/ */
public $relations = array(); 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) * @var array column aliases (alias => original name)
*/ */
public $columnAliases = array(); public $columnAliases = array();

16
tests/unit/framework/db/ar/ActiveRecordTest.php

@ -225,18 +225,21 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
public function testEagerLoading() public function testEagerLoading()
{ {
// has many
$customers = Customer::find()->with('orders')->order('@.id')->all(); $customers = Customer::find()->with('orders')->order('@.id')->all();
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertEquals(1, count($customers[0]->orders)); $this->assertEquals(1, count($customers[0]->orders));
$this->assertEquals(2, count($customers[1]->orders)); $this->assertEquals(2, count($customers[1]->orders));
$this->assertEquals(0, count($customers[2]->orders)); $this->assertEquals(0, count($customers[2]->orders));
// nested
$customers = Customer::find()->with('orders.customer')->order('@.id')->all(); $customers = Customer::find()->with('orders.customer')->order('@.id')->all();
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertEquals(1, $customers[0]->orders[0]->customer->id); $this->assertEquals(1, $customers[0]->orders[0]->customer->id);
$this->assertEquals(2, $customers[1]->orders[0]->customer->id); $this->assertEquals(2, $customers[1]->orders[0]->customer->id);
$this->assertEquals(2, $customers[1]->orders[1]->customer->id); $this->assertEquals(2, $customers[1]->orders[1]->customer->id);
// has many via relation
$orders = Order::find()->with('items')->order('@.id')->all(); $orders = Order::find()->with('items')->order('@.id')->all();
$this->assertEquals(3, count($orders)); $this->assertEquals(3, count($orders));
$this->assertEquals(1, $orders[0]->items[0]->id); $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(4, $orders[1]->items[1]->id);
$this->assertEquals(5, $orders[1]->items[2]->id); $this->assertEquals(5, $orders[1]->items[2]->id);
// has many via join table
$orders = Order::find()->with('books')->order('@.id')->all(); $orders = Order::find()->with('books')->order('@.id')->all();
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
$this->assertEquals(1, $orders[0]->books[0]->id); $this->assertEquals(1, $orders[0]->books[0]->id);
$this->assertEquals(2, $orders[0]->books[1]->id); $this->assertEquals(2, $orders[0]->books[1]->id);
$this->assertEquals(2, $orders[1]->books[0]->id); $this->assertEquals(2, $orders[1]->books[0]->id);
// has many and base limited
$orders = Order::find()->with('items')->order('@.id')->limit(2)->all(); $orders = Order::find()->with('items')->order('@.id')->limit(2)->all();
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
// findBySql with
$orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all();
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
// index and array
$customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all(); $customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all();
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertTrue(isset($customers[1], $customers[2], $customers[3])); $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(2, count($customers[2]['orders']));
$this->assertEquals(0, count($customers[3]['orders'])); $this->assertEquals(0, count($customers[3]['orders']));
$this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); $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() public function testLazyLoading()

Loading…
Cancel
Save