Browse Source

...

tags/2.0.0-beta
Qiang Xue 13 years ago
parent
commit
233183995a
  1. 2
      framework/base/Object.php
  2. 113
      framework/db/ar/ActiveFinder.php
  3. 10
      framework/db/ar/ActiveRecord.php
  4. 141
      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) 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);
} }
/** /**

113
framework/db/ar/ActiveFinder.php

@ -21,6 +21,30 @@ use yii\db\Exception;
* todo: clean up joinOnly and select=false * todo: clean up joinOnly and select=false
* todo: records for index != null, asArray = true * todo: records for index != null, asArray = true
* todo: refactor code * 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
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
@ -101,10 +125,38 @@ class ActiveFinder extends \yii\base\Object
return $records; return $records;
} }
public function findRelatedRecords($record, $relation)
public function findRelatedRecords($record, $relation, $params)
{ {
$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; private $_joinCount;
@ -150,7 +202,7 @@ class ActiveFinder extends \yii\base\Object
} }
$this->buildJoinTree($joinTree, $query->with); $this->buildJoinTree($joinTree, $query->with);
$this->initJoinTree($joinTree); $this->initJoinTree($joinTree, !isset($records));
$q = new Query; $q = new Query;
$this->buildJoinQuery($joinTree, $q); $this->buildJoinQuery($joinTree, $q);
@ -160,19 +212,9 @@ class ActiveFinder extends \yii\base\Object
} }
$rows = $q->createCommand($this->connection)->queryAll(); $rows = $q->createCommand($this->connection)->queryAll();
foreach ($rows as $row) { $joinTree->populateData($rows);
$joinTree->createRecord($row);
}
if ($query->index !== null) { return $query->index === null ? array_values($joinTree->records) : $joinTree->records;
$records = array();
foreach ($joinTree->records as $record) {
$records[$record[$query->index]] = $record;
}
return $records;
} else {
return array_values($joinTree->records);
}
} }
protected function applyScopes($query) protected function applyScopes($query)
@ -208,6 +250,9 @@ class ActiveFinder extends \yii\base\Object
*/ */
protected function buildJoinTree($parent, $with, $config = array()) protected function buildJoinTree($parent, $with, $config = array())
{ {
if (empty($with)) {
return null;
}
if (is_array($with)) { if (is_array($with)) {
foreach ($with as $name => $value) { foreach ($with as $name => $value) {
if (is_array($value)) { if (is_array($value)) {
@ -274,8 +319,9 @@ class ActiveFinder extends \yii\base\Object
/** /**
* @param JoinElement $element * @param JoinElement $element
* @param boolean $applyScopes
*/ */
protected function initJoinTree($element) protected function initJoinTree($element, $applyScopes = true)
{ {
if ($element->query->tableAlias !== null) { if ($element->query->tableAlias !== null) {
$alias = $element->query->tableAlias; $alias = $element->query->tableAlias;
@ -299,7 +345,13 @@ class ActiveFinder extends \yii\base\Object
$this->_tableAliases[$alias] = true; $this->_tableAliases[$alias] = true;
$element->query->tableAlias = $alias; $element->query->tableAlias = $alias;
if ($applyScopes) {
$this->applyScopes($element->query); $this->applyScopes($element->query);
}
if ($element->container !== null && $element->query->asArray === null) {
$element->query->asArray = $element->container->query->asArray;
}
foreach ($element->children as $child) { foreach ($element->children as $child) {
$this->initJoinTree($child, $count); $this->initJoinTree($child, $count);
@ -444,24 +496,26 @@ class ActiveFinder extends \yii\base\Object
$columns = array(); $columns = array();
$columnCount = 0; $columnCount = 0;
$prefix = $element->query->tableAlias; $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 === '*') { if (empty($select) || $select === '*') {
foreach ($table->columns as $column) { foreach ($table->columns as $column) {
if (!isset($element->pkAlias[$column->name])) {
$alias = "c{$element->id}_" . ($columnCount++); $alias = "c{$element->id}_" . ($columnCount++);
$columns[] = "$prefix.{$column->name} AS $alias"; $columns[] = "$prefix.{$column->name} AS $alias";
$element->columnAliases[$alias] = $column->name; $element->columnAliases[$alias] = $column->name;
if ($column->isPrimaryKey) {
$element->pkAlias[$column->name] = $alias;
} }
} }
} else { } else {
if (is_string($select)) { if (is_string($select)) {
$select = explode(',', $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) { foreach ($select as $column) {
$column = trim($column); $column = trim($column);
if (preg_match('/^(.*?)\s+AS\s+(\w+)$/im', $column, $matches)) { 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; return $columns;
} }
@ -506,7 +572,6 @@ class ActiveFinder extends \yii\base\Object
$query->andWhere(array('in', $prefix . $name, $values)); $query->andWhere(array('in', $prefix . $name, $values));
} else { } else {
$ors = array('or'); $ors = array('or');
$prefix = $this->connection->quoteTableName($activeQuery->tableAlias, true) . '.';
foreach ($rows as $row) { foreach ($rows as $row) {
$hash = array(); $hash = array();
foreach ($table->primaryKey as $name) { foreach ($table->primaryKey as $name) {

10
framework/db/ar/ActiveRecord.php

@ -555,8 +555,12 @@ abstract class ActiveRecord extends Model
} }
$relation = $md->relations[$relation]; $relation = $md->relations[$relation];
} }
$query = $this->createActiveQuery(); foreach ($params as $name => $value) {
return $query->findRelatedRecords($this, $relation, $params); $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) { foreach ($row as $name => $value) {
if (isset($columns[$name])) { if (isset($columns[$name])) {
$record->_attributes[$name] = $value; $record->_attributes[$name] = $value;
} elseif ($record->canSetProperty($name)) { } else {
$record->$name = $value; $record->$name = $value;
} }
} }

141
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 * @var JoinElement the parent element that this element needs to join with
*/ */
public $parent; public $parent;
public $container;
/** /**
* @var JoinElement[] the child elements that need to join with this element * @var JoinElement[] the child elements that need to join with this element
*/ */
public $children = array(); 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(); 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 $joinOnly;
/**
public $columnAliases = array(); // alias => original name * @var array column aliases (alias => original name)
public $pkAlias = array(); // original name => alias */
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 $records;
public $relatedRecords; public $related;
/** /**
* @param integer $id * @param integer $id
@ -60,71 +72,106 @@ class JoinElement extends \yii\base\Object
$this->query = $query; $this->query = $query;
if ($parent !== null) { if ($parent !== null) {
$this->parent = $parent; $this->parent = $parent;
$this->container = $container;
$parent->children[$query->name] = $this; $parent->children[$query->name] = $this;
if ($query->select !== false) {
$container->relations[$query->name] = $this; $container->relations[$query->name] = $this;
} }
} }
}
/** public function populateData($rows)
* @param array $row
* @return null|ActiveRecord
*/
public function createRecord($row)
{ {
$pk = array(); if ($this->container === null) {
foreach ($this->pkAlias as $alias) { foreach ($rows as $row) {
if (isset($row[$alias])) { if (($key = $this->getKeyValue($row)) !== null && !isset($this->records[$key])) {
$pk[] = $row[$alias]; $this->records[$key] = $this->createRecord($row);
}
}
} else { } else {
return null; foreach ($rows as $row) {
$key = $this->getKeyValue($row);
$containerKey = $this->container->getKeyValue($row);
if ($key === null || $containerKey === null || isset($this->related[$containerKey][$key])) {
continue;
} }
$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];
} }
$pk = count($pk) === 1 ? $pk[0] : serialize($pk);
// create record
// todo: asArray
if (isset($this->records[$pk])) {
$record = $this->records[$pk];
} else { } else {
$attributes = array(); $this->container->records[$containerKey][$this->query->name] =& $this->records[$key];
foreach ($row as $alias => $value) {
if (isset($this->columnAliases[$alias])) {
$attributes[$this->columnAliases[$alias]] = $value;
} }
} 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];
} }
$modelClass = $this->query->modelClass; } else {
$this->records[$pk] = $record = $modelClass::create($attributes); $this->container->records[$containerKey][$this->query->name] = $record;
foreach ($this->children as $child) { $this->records[$key] =& $this->container->records[$containerKey][$this->query->name];
if ($child->query->select !== false && !$child->joinOnly) { }
$record->initRelation($child->query); }
} 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) { foreach ($this->relations as $child) {
if ($child->query->select === false || $child->joinOnly) { $child->populateData($rows);
continue;
} }
$childRecord = $child->createRecord($row);
if ($childRecord === null) {
continue;
} }
if ($child->query->hasMany) {
if ($child->query->index !== null) { protected function getKeyValue($row)
$hash = $childRecord[$child->query->index]; {
} else { if (is_array($this->key)) {
$hash = serialize($childRecord->getPrimaryKey()); $key = array();
foreach ($this->key as $alias) {
if (!isset($row[$alias])) {
return null;
} }
if (!isset($this->relatedRecords[$pk][$child->query->name][$hash])) { $key[] = $row[$alias];
$this->relatedRecords[$pk][$child->query->name][$hash] = true;
$record->addRelatedRecord($child->query, $childRecord);
} }
return serialize($key);
} else { } else {
$record->addRelatedRecord($child->query, $childRecord); 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; 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__); \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__);
echo $sql . "\n\n";
try { try {
if ($this->connection->enableProfiling) { if ($this->connection->enableProfiling) {
\Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); \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(); $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all();
$this->assertEquals(2, count($orders)); $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