Browse Source

...

tags/2.0.0-beta
Qiang Xue 13 years ago
parent
commit
233183995a
  1. 2
      framework/base/Object.php
  2. 121
      framework/db/ar/ActiveFinder.php
  3. 10
      framework/db/ar/ActiveRecord.php
  4. 157
      framework/db/ar/JoinElement.php
  5. 2
      framework/db/dao/Command.php
  6. 9
      tests/unit/framework/db/ar/ActiveRecordTest.php

2
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);
}
/**

121
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 <qiang.xue@gmail.com>
@ -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) {

10
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;
}
}

157
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;
}
}

2
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__);

9
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']));
}
/*

Loading…
Cancel
Save