Browse Source

...

tags/2.0.0-beta
Qiang Xue 13 years ago
parent
commit
c5cdcab6c6
  1. 14
      framework/db/ar/ActiveMetaData.php
  2. 96
      framework/db/ar/ActiveQuery.php
  3. 147
      framework/db/ar/ActiveRecord.php
  4. 93
      framework/db/ar/JoinElement.php
  5. 2
      framework/db/dao/QueryBuilder.php
  6. 9
      tests/unit/data/ar/Customer.php
  7. 9
      tests/unit/data/ar/Order.php
  8. 2
      tests/unit/data/mysql.sql
  9. 12
      tests/unit/framework/db/ar/ActiveRecordTest.php

14
framework/db/ar/ActiveMetaData.php

@ -18,6 +18,10 @@ class ActiveMetaData
*/
public $table;
/**
* @var string the model class name
*/
public $modelClass;
/**
* @var array list of relations
*/
public $relations = array();
@ -30,6 +34,7 @@ class ActiveMetaData
{
$tableName = $modelClass::tableName();
$this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName);
$this->modelClass = $modelClass;
if ($this->table === null) {
throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'.");
}
@ -64,11 +69,16 @@ class ActiveMetaData
}
$relation = ActiveRelation::newInstance($config);
$relation->name = $matches[1];
$relation->modelClass = '\\' . $matches[2];
$modelClass = $matches[2];
if (strpos($modelClass, '\\') !== false) {
$relation->modelClass = '\\' . ltrim($modelClass, '\\');
} else {
$relation->modelClass = dirname($this->modelClass) . '\\' . $modelClass;
}
$relation->hasMany = isset($matches[3]);
$this->relations[$relation->name] = $relation;
} else {
throw new Exception("Relation name in bad format: $name");
throw new Exception("{$this->modelClass} has an invalid relation: $name");
}
}
}

96
framework/db/ar/ActiveQuery.php

@ -18,6 +18,18 @@ use yii\db\Exception;
* ActiveFinder.php is ...
* todo: add SQL monitor
*
* todo: add ActiveQueryBuilder
* todo: quote join/on part of the relational query
* todo: modify QueryBuilder about join() methods
* todo: unify ActiveQuery 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
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
@ -692,7 +704,7 @@ class ActiveQuery extends \yii\base\Object implements \IteratorAggregate, \Array
{
if (!empty($this->with)) {
// todo: handle findBySql() and limit cases
$this->initRelationalQuery();
$joinTree = $this->buildRelationalQuery();
}
if ($this->sql === null) {
@ -703,9 +715,16 @@ class ActiveQuery extends \yii\base\Object implements \IteratorAggregate, \Array
$command = $this->getDbConnection()->createCommand($this->sql);
$command->bindValues($this->query->params);
}
echo $command->sql;
$rows = $command->queryAll();
if (!empty($this->with)) {
foreach ($rows as $row) {
$joinTree->populateData($row);
}
return array_values($joinTree->records);
}
if ($this->asArray) {
if ($this->indexBy === null) {
return $rows;
@ -759,45 +778,76 @@ class ActiveQuery extends \yii\base\Object implements \IteratorAggregate, \Array
}
}
protected function initRelationalQuery()
protected function buildRelationalQuery()
{
$joinTree = new JoinElement(null, null);
$joinCount = 0;
$this->buildJoinTree($joinTree, $this->with, $joinCount);
$joinTree = new JoinElement($this, null, null);
$this->buildJoinTree($joinTree, $this->with);
$this->buildTableAlias($joinTree);
$query = new Query;
foreach ($joinTree->children as $child) {
$child->buildQuery($query);
}
$select = $joinTree->buildSelect($this->query->select);
if (!empty($query->select)) {
$this->query->select = array_merge($select, $query->select);
} else {
$this->query->select = $select;
}
if (!empty($query->where)) {
$this->query->andWhere('(' . implode(') AND (', $query->where) . ')');
}
if (!empty($query->having)) {
$this->query->andHaving('(' . implode(') AND (', $query->having) . ')');
}
if (!empty($query->join)) {
if ($this->query->join === null) {
$this->query->join = $query->join;
} else {
$this->query->join = array_merge($this->query->join, $query->join);
}
}
if (!empty($query->orderBy)) {
$this->query->addOrderBy($query->orderBy);
}
if (!empty($query->groupBy)) {
$this->query->addGroupBy($query->groupBy);
}
if (!empty($query->params)) {
$this->query->addParams($query->params);
}
return $joinTree;
}
/**
* @param JoinElement $parent
* @param array|string $with
* @param integer $joinCount
* @param array $config
* @return null|JoinElement
* @throws \yii\db\Exception
*/
protected function buildJoinTree($parent, $with, &$joinCount, $config = array())
protected function buildJoinTree($parent, $with, $config = array())
{
if (is_array($with)) {
foreach ($with as $name => $value) {
if (is_string($value)) {
$this->buildJoinTree($parent, $value, $joinCount);
$this->buildJoinTree($parent, $value);
} elseif (is_string($name) && is_array($value)) {
$this->buildJoinTree($parent, $name, $joinCount, $value);
$this->buildJoinTree($parent, $name, $value);
}
}
return null;
}
if (($pos = strrpos($with, '.')) !== false) {
$parent = $this->buildJoinTree($parent, substr($with, 0, $pos), $joinCount);
$parent = $this->buildJoinTree($parent, substr($with, 0, $pos));
$with = substr($with, $pos + 1);
}
if (isset($parent->children[$with])) {
$child = $parent->children[$with];
$child->joinOnly = false;
} else {
$modelClass = $parent->relation->modelClass;
$relations = $modelClass::getMetaData()->relations;
@ -805,20 +855,32 @@ class ActiveQuery extends \yii\base\Object implements \IteratorAggregate, \Array
throw new Exception("$modelClass has no relation named '$with'.");
}
$relation = clone $relations[$with];
if ($relation->tableAlias === null) {
$relation->tableAlias = 't' . ($joinCount++);
if ($relation->via !== null && isset($relations[$relation->via])) {
$relation->via = null;
$parent2 = $this->buildJoinTree($parent, $relation->via);
if ($parent2->joinOnly === null) {
$parent2->joinOnly = true;
}
$child = new JoinElement($relation, $parent2, $parent);
} else {
$child = new JoinElement($relation, $parent, $parent);
}
$child = new JoinElement($parent, $relation);
}
foreach ($config as $name => $value) {
$child->relation->$name = $value;
}
if (!empty($child->relation->with)) {
$this->buildJoinTree($child, $child->relation->with, $joinCount);
return $child;
}
return $child;
protected function buildTableAlias($element, &$count = 0)
{
if ($element->relation->tableAlias === null) {
$element->relation->tableAlias = 't' . ($count++);
}
foreach ($element->children as $child) {
$this->buildTableAlias($child, $count);
}
}
}

147
framework/db/ar/ActiveRecord.php

@ -373,21 +373,13 @@ abstract class ActiveRecord extends \yii\base\Model
{
if (isset($this->_attributes[$name])) {
return true;
}
elseif (isset($this->getMetaData()->columns[$name]))
{
} elseif (isset($this->getMetaData()->columns[$name])) {
return false;
}
elseif (isset($this->_related[$name]))
{
} elseif (isset($this->_related[$name])) {
return true;
}
elseif (isset($this->getMetaData()->relations[$name]))
{
} elseif (isset($this->getMetaData()->relations[$name])) {
return $this->getRelatedRecord($name) !== null;
}
else
{
} else {
return parent::__isset($name);
}
}
@ -402,13 +394,9 @@ abstract class ActiveRecord extends \yii\base\Model
{
if (isset($this->getMetaData()->columns[$name])) {
unset($this->_attributes[$name]);
}
elseif (isset($this->getMetaData()->relations[$name]))
{
} elseif (isset($this->getMetaData()->relations[$name])) {
unset($this->_related[$name]);
}
else
{
} else {
parent::__unset($name);
}
}
@ -426,9 +414,7 @@ abstract class ActiveRecord extends \yii\base\Model
if (isset($this->getMetaData()->relations[$name])) {
if (empty($parameters)) {
return $this->getRelatedRecord($name, false);
}
else
{
} else {
return $this->getRelatedRecord($name, false, $parameters[0]);
}
}
@ -442,6 +428,20 @@ abstract class ActiveRecord extends \yii\base\Model
return parent::__call($name, $parameters);
}
public function initRelatedRecord($relation)
{
$this->_related[$relation->name] = $relation->hasMany ? array() : null;
}
public function addRelatedRecord($relation, $record)
{
if ($relation->hasMany) {
$this->_related[$relation->name][] = $record;
} else {
$this->_related[$relation->name] = $record;
}
}
/**
* Returns the related record(s).
* This method will return the related record(s) of the current record.
@ -464,8 +464,7 @@ abstract class ActiveRecord extends \yii\base\Model
$md = $this->getMetaData();
if (!isset($md->relations[$name])) {
throw new Exception(Yii::t('yii', '{class} does not have relation "{name}".',
array('{class}' => get_class($this), '{name}' => $name)));
throw new Exception(Yii::t('yii', '{class} does not have relation "{name}".', array('{class}' => get_class($this), '{name}' => $name)));
}
Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'system.db.ar.ActiveRecord');
@ -481,8 +480,7 @@ abstract class ActiveRecord extends \yii\base\Model
$save = $this->_related[$name];
}
$r = array($name => $params);
} else
{
} else {
$r = $name;
}
unset($this->_related[$name]);
@ -493,13 +491,9 @@ abstract class ActiveRecord extends \yii\base\Model
if (!isset($this->_related[$name])) {
if ($relation instanceof CHasManyRelation) {
$this->_related[$name] = array();
}
elseif ($relation instanceof CStatRelation)
{
} elseif ($relation instanceof CStatRelation) {
$this->_related[$name] = $relation->defaultValue;
}
else
{
} else {
$this->_related[$name] = null;
}
}
@ -508,14 +502,11 @@ abstract class ActiveRecord extends \yii\base\Model
$results = $this->_related[$name];
if ($exists) {
$this->_related[$name] = $save;
}
else
{
} else {
unset($this->_related[$name]);
}
return $results;
} else
{
} else {
return $this->_related[$name];
}
}
@ -617,9 +608,7 @@ abstract class ActiveRecord extends \yii\base\Model
{
if (property_exists($this, $name)) {
return $this->$name;
}
elseif (isset($this->_attributes[$name]))
{
} elseif (isset($this->_attributes[$name])) {
return $this->_attributes[$name];
}
}
@ -636,13 +625,9 @@ abstract class ActiveRecord extends \yii\base\Model
{
if (property_exists($this, $name)) {
$this->$name = $value;
}
elseif (isset($this->getMetaData()->table->columns[$name]))
{
} elseif (isset($this->getMetaData()->table->columns[$name])) {
$this->_attributes[$name] = $value;
}
else
{
} else {
return false;
}
return true;
@ -660,31 +645,24 @@ abstract class ActiveRecord extends \yii\base\Model
public function getAttributes($names = true)
{
$attributes = $this->_attributes;
foreach ($this->getMetaData()->columns as $name => $column)
{
foreach ($this->getMetaData()->columns as $name => $column) {
if (property_exists($this, $name)) {
$attributes[$name] = $this->$name;
}
elseif ($names === true && !isset($attributes[$name]))
{
} elseif ($names === true && !isset($attributes[$name])) {
$attributes[$name] = null;
}
}
if (is_array($names)) {
$attrs = array();
foreach ($names as $name)
{
foreach ($names as $name) {
if (property_exists($this, $name)) {
$attrs[$name] = $this->$name;
}
else
{
} else {
$attrs[$name] = isset($attributes[$name]) ? $attributes[$name] : null;
}
}
return $attrs;
} else
{
} else {
return $attributes;
}
}
@ -716,9 +694,7 @@ abstract class ActiveRecord extends \yii\base\Model
{
if (!$runValidation || $this->validate($attributes)) {
return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes);
}
else
{
} else {
return false;
}
}
@ -798,8 +774,7 @@ abstract class ActiveRecord extends \yii\base\Model
$event = new CModelEvent($this);
$this->onBeforeSave($event);
return $event->isValid;
} else
{
} else {
return true;
}
}
@ -830,8 +805,7 @@ abstract class ActiveRecord extends \yii\base\Model
$event = new CModelEvent($this);
$this->onBeforeDelete($event);
return $event->isValid;
} else
{
} else {
return true;
}
}
@ -910,11 +884,8 @@ abstract class ActiveRecord extends \yii\base\Model
if ($table->sequenceName !== null) {
if (is_string($primaryKey) && $this->$primaryKey === null) {
$this->$primaryKey = $builder->getLastInsertID($table);
}
elseif (is_array($primaryKey))
{
foreach ($primaryKey as $pk)
{
} elseif (is_array($primaryKey)) {
foreach ($primaryKey as $pk) {
if ($this->$pk === null) {
$this->$pk = $builder->getLastInsertID($table);
break;
@ -955,8 +926,7 @@ abstract class ActiveRecord extends \yii\base\Model
$this->_pk = $this->getPrimaryKey();
$this->afterSave();
return true;
} else
{
} else {
return false;
}
}
@ -984,13 +954,10 @@ abstract class ActiveRecord extends \yii\base\Model
if (!$this->getIsNewRecord()) {
Yii::trace(get_class($this) . '.saveAttributes()', 'system.db.ar.ActiveRecord');
$values = array();
foreach ($attributes as $name => $value)
{
foreach ($attributes as $name => $value) {
if (is_integer($name)) {
$values[$value] = $this->$value;
}
else
{
} else {
$values[$name] = $this->$name = $value;
}
}
@ -1000,12 +967,10 @@ abstract class ActiveRecord extends \yii\base\Model
if ($this->updateByPk($this->getOldPrimaryKey(), $values) > 0) {
$this->_pk = $this->getPrimaryKey();
return true;
} else
{
} else {
return false;
}
} else
{
} else {
throw new Exception(Yii::t('yii', 'The active record cannot be updated because it is new.'));
}
}
@ -1032,13 +997,11 @@ abstract class ActiveRecord extends \yii\base\Model
$criteria = $builder->createPkCriteria($table, $this->getOldPrimaryKey());
$command = $builder->createUpdateCounterCommand($this->getTableSchema(), $counters, $criteria);
if ($command->execute()) {
foreach ($counters as $name => $value)
{
foreach ($counters as $name => $value) {
$this->$name = $this->$name + $value;
}
return true;
} else
{
} else {
return false;
}
}
@ -1056,12 +1019,10 @@ abstract class ActiveRecord extends \yii\base\Model
$result = $this->deleteByPk($this->getPrimaryKey()) > 0;
$this->afterDelete();
return $result;
} else
{
} else {
return false;
}
} else
{
} else {
throw new Exception(Yii::t('yii', 'The active record cannot be deleted because it is new.'));
}
}
@ -1076,19 +1037,15 @@ abstract class ActiveRecord extends \yii\base\Model
if (!$this->getIsNewRecord() && ($record = $this->findByPk($this->getPrimaryKey())) !== null) {
$this->_attributes = array();
$this->_related = array();
foreach ($this->getMetaData()->columns as $name => $column)
{
foreach ($this->getMetaData()->columns as $name => $column) {
if (property_exists($this, $name)) {
$this->$name = $record->$name;
}
else
{
} else {
$this->_attributes[$name] = $record->$name;
}
}
return true;
} else
{
} else {
return false;
}
}

93
framework/db/ar/JoinElement.php

@ -22,13 +22,21 @@ class JoinElement extends \yii\base\Object
*/
public $relation;
/**
* @var JoinElement
* @var JoinElement the parent element that this element needs to join with
*/
public $parent;
/**
* @var JoinElement[]
* @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
*/
public $relatedChildren = array();
/**
* @var boolean whether this element is only for join purpose. If true, data will also be populated into the AR of this element.
*/
public $joinOnly;
public $columnAliases = array(); // alias => original name
public $pkAlias = array(); // original name => alias
@ -36,12 +44,13 @@ class JoinElement extends \yii\base\Object
public $records;
public $relatedRecords;
public function __construct($parent, $relation)
public function __construct($relation, $parent, $relatedParent)
{
$this->relation = $relation;
if ($parent !== null) {
$this->parent = $parent;
$this->relation = $relation;
$parent->children[$relation->name] = $this;
$relatedParent->relatedChildren[$relation->name] = $this;
}
}
@ -68,18 +77,18 @@ class JoinElement extends \yii\base\Object
}
}
$modelClass = $this->relation->modelClass;
$record = $modelClass::populateRecord($attributes);
$record = $modelClass::populateData($attributes);
foreach ($this->children as $child) {
if ($child->relation->select !== false) {
$record->initRelation($child->relation);
$record->initRelatedRecord($child->relation);
}
}
$this->records[$pk] = $record;
}
// populate child records
foreach ($this->children as $child) {
if ($child->relation->select === false) {
foreach ($this->relatedChildren as $child) {
if ($child->relation->select === false || $child->joinOnly) {
continue;
}
$childRecord = $child->populateData($row);
@ -102,10 +111,10 @@ class JoinElement extends \yii\base\Object
public function buildQuery($query)
{
$tokens = array(
'@.' => $this->relation->tableAlias,
'?.' => $this->parent->relation->tableAlias,
'@.' => $this->relation->tableAlias . '.',
'?.' => $this->parent->relation->tableAlias . '.',
);
foreach ($this->buildSelect() as $column) {
foreach ($this->buildSelect($this->relation->select) as $column) {
$query->select[] = strtr($column, $tokens);
}
@ -117,32 +126,60 @@ class JoinElement extends \yii\base\Object
$query->having[] = strtr($this->relation->having, $tokens);
}
/*
* joinType;
on;
via;
orderby
groupby
join
params
*/
if ($this->relation->via !== null) {
$query->join[] = $this->relation->via;
}
$modelClass = $this->relation->modelClass;
$tableName = $modelClass::tableName();
$joinType = $this->relation->joinType === null ? 'LEFT JOIN' : $this->relation->joinType;
$join = "$joinType $tableName {$this->relation->tableAlias}";
if ($this->relation->on !== null) {
$join .= ' ON ' . strtr($this->relation->on, $tokens);
}
$query->join[] = $join;
if ($this->relation->join !== null) {
$query->join[] = strtr($this->relation->join, $tokens);
}
// todo: convert orderBy to array first
if ($this->relation->orderBy !== null) {
$query->orderBy[] = strtr($this->relation->orderBy, $tokens);
}
// todo: convert groupBy to array first
if ($this->relation->groupBy !== null) {
$query->groupBy[] = strtr($this->relation->groupBy, $tokens);
}
if ($this->relation->params !== null) {
foreach ($this->relation->params as $name => $value) {
if (is_integer($name)) {
$query->params[] = $value;
} else {
$query->params[$name] = $value;
}
}
}
foreach ($this->children as $child) {
$child->buildQuery($query);
}
}
public function buildSelect()
public function buildSelect($select)
{
$modelClass = $this->relation->modelClass;
$tableSchema = $modelClass::getMetaData()->table;
$select = $this->relation->select;
$columns = array();
$columnCount = 0;
$prefix = $this->relation->tableAlias;
if (empty($select) || $select === '*') {
foreach ($tableSchema->columns as $column) {
$alias = $this->tableAlias . '_' . ($columnCount++);
$columns[] = "{$column->name} AS $alias";
$alias = $this->relation->tableAlias . '_' . ($columnCount++);
$columns[] = "$prefix.{$column->name} AS $alias";
$this->columnAliases[$alias] = $column->name;
if ($column->isPrimaryKey) {
$this->pkAlias[$column->name] = $alias;
@ -153,8 +190,8 @@ class JoinElement extends \yii\base\Object
$select = explode(',', $select);
}
foreach ($tableSchema->primaryKey as $column) {
$alias = $this->tableAlias . '_' . ($columnCount++);
$columns[] = "$column AS $alias";
$alias = $this->relation->tableAlias . '_' . ($columnCount++);
$columns[] = "$prefix.$column AS $alias";
$this->pkAlias[$column] = $alias;
}
foreach ($select as $column) {
@ -164,8 +201,8 @@ class JoinElement extends \yii\base\Object
$this->columnAliases[$matches[2]] = $matches[2];
$columns[] = $column;
} elseif (!isset($this->pkAlias[$column])) {
$alias = $this->tableAlias . '_' . ($columnCount++);
$columns[] = "$column AS $alias";
$alias = $this->relation->tableAlias . '_' . ($columnCount++);
$columns[] = "$prefix.$column AS $alias";
$this->columnAliases[$alias] = $column;
}
}

2
framework/db/dao/QueryBuilder.php

@ -656,7 +656,7 @@ class QueryBuilder extends \yii\base\Object
if (is_object($column)) {
$columns[$i] = (string)$column;
} elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-\.])$/', $column, $matches)) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
$columns[$i] = $driver->quoteColumnName($matches[1]) . ' AS ' . $driver->quoteSimpleColumnName($matches[2]);
} else {
$columns[$i] = $driver->quoteColumnName($column);

9
tests/unit/data/ar/Customer.php

@ -8,4 +8,13 @@ class Customer extends ActiveRecord
{
return 'tbl_customer';
}
public static function relations()
{
return array(
'orders:Order[]' => array(
'on' => '@.customer_id = ?.id',
),
);
}
}

9
tests/unit/data/ar/Order.php

@ -8,4 +8,13 @@ class Order extends ActiveRecord
{
return 'tbl_order';
}
public static function relations()
{
return array(
'customer:Customer' => array(
'on' => '@.id = ?.customer_id',
),
);
}
}

2
tests/unit/data/mysql.sql

@ -86,7 +86,7 @@ INSERT INTO tbl_item (name, category_id) VALUES ('Cars', 2);
INSERT INTO tbl_order (customer_id, create_time, total) VALUES (1, 1325282384, 110.0);
INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325334482, 33.0);
INSERT INTO tbl_order (customer_id, create_time, total) VALUES (3, 1325502201, 40.0);
INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325502201, 40.0);
INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0);
INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0);

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

@ -116,6 +116,18 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertEquals(3, $customer->id);
$this->assertEquals(null, $customer->name);
}
public function testEagerLoading()
{
$customers = Customer::find()->with('orders')->orderBy('t0.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));
$customers = Customer::find()->with('orders.customer')->orderBy('t0.id')->all();
}
/*
public function testGetSql()
{

Loading…
Cancel
Save