Browse Source

new AR WIP

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
a8be9ce412
  1. 450
      framework/db/ar/ActiveQuery.php
  2. 31
      framework/db/ar/ActiveRecord.php
  3. 8
      framework/db/ar/HasManyRelation.php
  4. 8
      framework/db/ar/HasOneRelation.php
  5. 10
      framework/db/ar/ManyManyRelation.php
  6. 14
      framework/db/ar/Relation.php
  7. 100
      framework/db/dao/BaseQuery.php
  8. 88
      tests/unit/framework/db/ar/ActiveRecordTest.php

450
framework/db/ar/ActiveQuery.php

@ -15,7 +15,7 @@ use yii\base\VectorIterator;
use yii\db\dao\Expression; use yii\db\dao\Expression;
use yii\db\Exception; use yii\db\Exception;
class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable class ActiveQuery extends BaseQuery
{ {
/** /**
* @var string the name of the ActiveRecord class. * @var string the name of the ActiveRecord class.
@ -24,7 +24,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
/** /**
* @var array list of relations that this query should be performed with * @var array list of relations that this query should be performed with
*/ */
public $with; public $with = array();
/** /**
* @var string the name of the column that the result should be indexed by. * @var string the name of the column that the result should be indexed by.
* This is only useful when the query result is returned as an array. * This is only useful when the query result is returned as an array.
@ -38,28 +38,12 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
/** /**
* @var array list of scopes that should be applied to this query * @var array list of scopes that should be applied to this query
*/ */
public $scopes; public $scopes = array();
/** /**
* @var string the SQL statement to be executed for retrieving AR records. * @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]]. * This is set by [[ActiveRecord::findBySql()]].
*/ */
public $sql; public $sql;
/**
* @var array list of query results. Depending on [[asArray]], this can be either
* an array of AR objects (when [[asArray]] is false) or an array of array
* (when [[asArray]] is true).
*/
public $records;
/**
* @param string $modelClass the name of the ActiveRecord class.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($modelClass, $config = array())
{
$this->modelClass = $modelClass;
parent::__construct($config);
}
public function __call($name, $params) public function __call($name, $params)
{ {
@ -77,7 +61,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
*/ */
public function all() public function all()
{ {
return $this->findRecords(); return $this->find();
} }
/** /**
@ -88,8 +72,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
*/ */
public function one() public function one()
{ {
$this->limit = 1; $records = $this->find();
$records = $this->findRecords();
return isset($records[0]) ? $records[0] : null; return isset($records[0]) ? $records[0] : null;
} }
@ -125,15 +108,6 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
} }
/** /**
* Returns the number of items in the vector.
* @return integer the number of items in the vector
*/
public function getCount()
{
return $this->count();
}
/**
* Sets the parameters about query caching. * Sets the parameters about query caching.
* This is a shortcut method to {@link CDbConnection::cache()}. * This is a shortcut method to {@link CDbConnection::cache()}.
* It changes the query caching parameter of the {@link dbConnection} instance. * It changes the query caching parameter of the {@link dbConnection} instance.
@ -150,139 +124,45 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
return $this; return $this;
} }
/** protected function find()
* Returns an iterator for traversing the items in the vector.
* This method is required by the SPL interface `IteratorAggregate`.
* It will be implicitly called when you use `foreach` to traverse the vector.
* @return VectorIterator an iterator for traversing the items in the vector.
*/
public function getIterator()
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return new VectorIterator($this->records);
}
/**
* Returns the number of items in the vector.
* This method is required by the SPL `Countable` interface.
* It will be implicitly called when you use `count($vector)`.
* @return integer number of items in the vector.
*/
public function count()
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return count($this->records);
}
/**
* Returns a value indicating whether there is an item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `isset($vector[$offset])`.
* @param integer $offset the offset to be checked
* @return boolean whether there is an item at the specified offset.
*/
public function offsetExists($offset)
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return isset($this->records[$offset]);
}
/**
* Returns the item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `$value = $vector[$offset];`.
* This is equivalent to [[itemAt]].
* @param integer $offset the offset to retrieve item.
* @return ActiveRecord the item at the offset
* @throws Exception if the offset is out of range
*/
public function offsetGet($offset)
{ {
if ($this->records === null) { $modelClass = $this->modelClass;
$this->records = $this->findRecords();
}
return isset($this->records[$offset]) ? $this->records[$offset] : null;
}
/** /**
* Sets the item at the specified offset. * @var ActiveRecord $model
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `$vector[$offset] = $item;`.
* If the offset is null or equal to the number of the existing items,
* the new item will be appended to the vector.
* Otherwise, the existing item at the offset will be replaced with the new item.
* @param integer $offset the offset to set item
* @param ActiveRecord $item the item value
* @throws Exception if the offset is out of range, or the vector is read only.
*/ */
public function offsetSet($offset, $item) $model = $modelClass::model();
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
$this->records[$offset] = $item;
}
/** /**
* Unsets the item at the specified offset. * @var \yii\db\dao\Connection $db
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `unset($vector[$offset])`.
* This is equivalent to [[removeAt]].
* @param integer $offset the offset to unset item
* @throws Exception if the offset is out of range, or the vector is read only.
*/ */
public function offsetUnset($offset) $db = $model->getDbConnection();
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
unset($this->records[$offset]);
}
public function find()
{
/**
* find the primary ARs
* for each child relation
* find the records filtered by the PK constraints
* populate primary ARs with the related records
* recursively call this metod again
*/
}
protected function findByParent($parent)
{
}
protected function findRecords()
{
if (!empty($this->with)) {
return $this->findWithRelations();
}
if ($this->sql === null) { if ($this->sql === null) {
if ($this->from === null) { if ($this->from === null) {
$modelClass = $this->modelClass; $tableName = $model->getTableSchema()->name;
$tableName = $modelClass::model()->getTableSchema()->name;
$this->from = array($tableName); $this->from = array($tableName);
} }
$this->sql = $this->connection->getQueryBuilder()->build($this); foreach ($this->scopes as $name => $config) {
if (is_integer($name)) {
$model->$config($this);
} else {
array_unshift($config, $this);
call_user_func_array(array($model, $name), $config);
}
}
$this->sql = $db->getQueryBuilder()->build($this);
} }
$command = $this->connection->createCommand($this->sql, $this->params); $command = $db->createCommand($this->sql, $this->params);
$rows = $command->queryAll(); $rows = $command->queryAll();
return $this->createRecords($rows); $records = $this->createRecords($rows);
foreach ($this->with as $name => $config) {
$relation = $model->$name();
foreach ($config as $p => $v) {
$relation->$p = $v;
}
$relation->findWith($records);
} }
protected function findWithRelations() return $records;
{
$records = $this->findRecords();
} }
protected function createRecords($rows) protected function createRecords($rows)
@ -311,271 +191,3 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
return $records; return $records;
} }
} }
/**
* 1. eager loading, base limited and has has_many relations
* 2.
* ActiveFinder.php is ...
*
* @property integer $count
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ActiveQuery2 extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable
{
/**
* @var string the SQL statement to be executed to retrieve primary records.
* This is set by [[ActiveRecord::findBySql()]].
*/
public $sql;
/**
* @var array list of query results
*/
public $records;
/**
* @param string $modelClass the name of the ActiveRecord class.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($modelClass, $config = array())
{
$this->modelClass = $modelClass;
parent::__construct($config);
}
public function __call($name, $params)
{
if (method_exists($this->modelClass, $name)) {
$this->scopes[$name] = $params;
return $this;
} else {
return parent::__call($name, $params);
}
}
/**
* Executes query and returns all results as an array.
* @return array the query results. If the query results in nothing, an empty array will be returned.
*/
public function all()
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return $this->records;
}
/**
* Executes query and returns a single row of result.
* @return null|array|ActiveRecord the single row of query result. Depending on the setting of [[asArray]],
* the query result may be either an array or an ActiveRecord object. Null will be returned
* if the query results in nothing.
*/
public function one()
{
if ($this->records === null) {
$this->limit = 1;
$this->records = $this->findRecords();
}
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()
{
return $this->createFinder()->find($this, true);
}
/**
* Executes query and returns if matching row exists in the table.
* @return bool if row exists in the table.
*/
public function exists()
{
return $this->select(array(new Expression('1')))->value() !== false;
}
/**
* Returns the database connection used by this query.
* This method returns the connection used by the [[modelClass]].
* @return \yii\db\dao\Connection the database connection used by this query
*/
public function getDbConnection()
{
$class = $this->modelClass;
return $class::getDbConnection();
}
/**
* Returns the number of items in the vector.
* @return integer the number of items in the vector
*/
public function getCount()
{
return $this->count();
}
/**
* Sets the parameters about query caching.
* This is a shortcut method to {@link CDbConnection::cache()}.
* It changes the query caching parameter of the {@link dbConnection} instance.
* @param integer $duration the number of seconds that query results may remain valid in cache.
* If this is 0, the caching will be disabled.
* @param \yii\caching\Dependency $dependency the dependency that will be used when saving the query results into cache.
* @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1,
* meaning that the next SQL query will be cached.
* @return ActiveRecord the active record instance itself.
*/
public function cache($duration, $dependency = null, $queryCount = 1)
{
$this->getDbConnection()->cache($duration, $dependency, $queryCount);
return $this;
}
/**
* Returns an iterator for traversing the items in the vector.
* This method is required by the SPL interface `IteratorAggregate`.
* It will be implicitly called when you use `foreach` to traverse the vector.
* @return VectorIterator an iterator for traversing the items in the vector.
*/
public function getIterator()
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return new VectorIterator($this->records);
}
/**
* Returns the number of items in the vector.
* This method is required by the SPL `Countable` interface.
* It will be implicitly called when you use `count($vector)`.
* @return integer number of items in the vector.
*/
public function count()
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return count($this->records);
}
/**
* Returns a value indicating whether there is an item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `isset($vector[$offset])`.
* @param integer $offset the offset to be checked
* @return boolean whether there is an item at the specified offset.
*/
public function offsetExists($offset)
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return isset($this->records[$offset]);
}
/**
* Returns the item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `$value = $vector[$offset];`.
* This is equivalent to [[itemAt]].
* @param integer $offset the offset to retrieve item.
* @return ActiveRecord the item at the offset
* @throws Exception if the offset is out of range
*/
public function offsetGet($offset)
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
return isset($this->records[$offset]) ? $this->records[$offset] : null;
}
/**
* Sets the item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `$vector[$offset] = $item;`.
* If the offset is null or equal to the number of the existing items,
* the new item will be appended to the vector.
* Otherwise, the existing item at the offset will be replaced with the new item.
* @param integer $offset the offset to set item
* @param ActiveRecord $item the item value
* @throws Exception if the offset is out of range, or the vector is read only.
*/
public function offsetSet($offset, $item)
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
$this->records[$offset] = $item;
}
/**
* Unsets the item at the specified offset.
* This method is required by the SPL interface `ArrayAccess`.
* It is implicitly called when you use something like `unset($vector[$offset])`.
* This is equivalent to [[removeAt]].
* @param integer $offset the offset to unset item
* @throws Exception if the offset is out of range, or the vector is read only.
*/
public function offsetUnset($offset)
{
if ($this->records === null) {
$this->records = $this->findRecords();
}
unset($this->records[$offset]);
}
protected function findRecords()
{
return $this->createFinder()->find($this);
}
protected function createFinder()
{
return new ActiveFinder($this->getDbConnection());
}
public function asArray($value = true)
{
$this->asArray = $value;
return $this;
}
public function with()
{
$this->with = func_get_args();
if (isset($this->with[0]) && is_array($this->with[0])) {
// the parameter is given as an array
$this->with = $this->with[0];
}
return $this;
}
public function index($column)
{
$this->index = $column;
return $this;
}
public function tableAlias($value)
{
$this->tableAlias = $value;
return $this;
}
public function scopes($names)
{
$this->scopes = $names;
return $this;
}
}

31
framework/db/ar/ActiveRecord.php

@ -252,7 +252,7 @@ abstract class ActiveRecord extends Model
*/ */
public static function createActiveQuery() public static function createActiveQuery()
{ {
return new ActiveQuery(get_called_class()); return new ActiveQuery(array('modelClass' => get_called_class()));
} }
/** /**
@ -382,6 +382,35 @@ abstract class ActiveRecord extends Model
} }
} }
public function hasOne($class, $link)
{
return new HasOneRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'link' => $link,
));
}
public function hasMany($class, $link)
{
return new HasManyRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'link' => $link,
));
}
public function manyMany($class, $leftLink, $joinTable, $rightLink)
{
return new ManyManyRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'leftLink' => $leftLink,
'joinTable' => $joinTable,
'rightLink' => $rightLink,
));
}
/** /**
* Initializes the internal storage for the relation. * Initializes the internal storage for the relation.
* This method is internally used by [[ActiveQuery]] when populating relation data. * This method is internally used by [[ActiveQuery]] when populating relation data.

8
framework/db/ar/HasManyRelation.php

@ -0,0 +1,8 @@
<?php
namespace yii\db\ar;
class HasManyRelation extends Relation
{
public $link;
}

8
framework/db/ar/HasOneRelation.php

@ -0,0 +1,8 @@
<?php
namespace yii\db\ar;
class HasOneRelation extends Relation
{
public $link;
}

10
framework/db/ar/ManyManyRelation.php

@ -0,0 +1,10 @@
<?php
namespace yii\db\ar;
class ManyManyRelation extends Relation
{
public $joinTable;
public $leftLink;
public $rightLink;
}

14
framework/db/ar/Relation.php

@ -0,0 +1,14 @@
<?php
namespace yii\db\ar;
class Relation extends ActiveQuery
{
public $parentClass;
public $parentRecords;
public function findWith($records)
{
}
}

100
framework/db/dao/BaseQuery.php

@ -532,103 +532,23 @@ class BaseQuery extends \yii\base\Component
/** /**
* Merges this query with another one. * Merges this query with another one.
* * If a property of `$query` is not null, it will be used to overwrite
* The merging is done according to the following rules: * the corresponding property of `$this`.
*
* - [[select]]: the union of both queries' [[select]] property values.
* - [[selectOption]], [[distinct]], [[from]], [[limit]], [[offset]]: the new query
* takes precedence over this query.
* - [[where]], [[having]]: the new query's corresponding property value
* will be 'AND' together with the existing one.
* - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's
* corresponding property value will be appended to the existing one.
*
* In general, the merging makes the resulting query more restrictive and specific.
* @param BaseQuery $query the new query to be merged with this query. * @param BaseQuery $query the new query to be merged with this query.
* @return BaseQuery the query object itself * @return BaseQuery the query object itself
*/ */
public function mergeWith(BaseQuery $query) public function mergeWith(BaseQuery $query)
{ {
if ($this->select !== $query->select) { $properties = array(
if (empty($this->select)) { 'select', 'selectOption', 'distinct', 'from',
$this->select = $query->select; 'where', 'limit', 'offset', 'orderBy', 'groupBy',
} elseif (!empty($query->select)) { 'join', 'having', 'union', 'params',
$select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; );
$select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; foreach ($properties as $name => $value) {
$this->select = array_merge($select1, array_diff($select2, $select1)); if ($value !== null) {
} $this->$name = $value;
}
if ($query->selectOption !== null) {
$this->selectOption = $query->selectOption;
}
if ($query->distinct !== null) {
$this->distinct = $query->distinct;
}
if ($query->from !== null) {
$this->from = $query->from;
}
if ($query->limit !== null) {
$this->limit = $query->limit;
}
if ($query->offset !== null) {
$this->offset = $query->offset;
}
if ($query->where !== null) {
$this->andWhere($query->where);
}
if ($query->having !== null) {
$this->andHaving($query->having);
}
if ($query->params !== null) {
$this->addParams($query->params);
}
if ($query->orderBy !== null) {
$this->addOrderBy($query->orderBy);
}
if ($query->groupBy !== null) {
$this->addGroup($query->groupBy);
}
if ($query->join !== null) {
if (empty($this->join)) {
$this->join = $query->join;
} else {
if (!is_array($this->join)) {
$this->join = array($this->join);
}
if (is_array($query->join)) {
$this->join = array_merge($this->join, $query->join);
} else {
$this->join[] = $query->join;
} }
} }
}
if ($query->union !== null) {
if (empty($this->union)) {
$this->union = $query->union;
} else {
if (!is_array($this->union)) {
$this->union = array($this->union);
}
if (is_array($query->union)) {
$this->union = array_merge($this->union, $query->union);
} else {
$this->union[] = $query->union;
}
}
}
return $this; return $this;
} }
} }

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

@ -23,42 +23,14 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($result instanceof ActiveQuery); $this->assertTrue($result instanceof ActiveQuery);
$customer = $result->one(); $customer = $result->one();
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(1, $result->count);
$this->assertEquals(1, count($result));
// find all // find all
$result = Customer::find(); $result = Customer::find();
$customers = $result->all(); $customers = $result->all();
$this->assertTrue(is_array($customers));
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertTrue($customers[0] instanceof Customer); $this->assertTrue($customers[0] instanceof Customer);
$this->assertTrue($customers[1] instanceof Customer); $this->assertTrue($customers[1] instanceof Customer);
$this->assertTrue($customers[2] instanceof Customer); $this->assertTrue($customers[2] instanceof Customer);
$this->assertEquals(3, $result->count);
$this->assertEquals(3, count($result));
// check count first
$result = Customer::find();
$this->assertEquals(3, $result->count);
$customer = $result->one();
$this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, $result->count);
// iterator
$result = Customer::find();
$count = 0;
foreach ($result as $customer) {
$this->assertTrue($customer instanceof Customer);
$count++;
}
$this->assertEquals($count, $result->count);
// array access
$result = Customer::find();
$this->assertTrue($result[0] instanceof Customer);
$this->assertTrue($result[1] instanceof Customer);
$this->assertTrue($result[2] instanceof Customer);
$this->assertEquals(3, count($result));
// find by a single primary key // find by a single primary key
$customer = Customer::find(2); $customer = Customer::find(2);
@ -70,7 +42,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(2, $customer->id); $this->assertEquals(2, $customer->id);
// find by Query // find by Query array
$query = array( $query = array(
'where' => 'id=:id', 'where' => 'id=:id',
'params' => array(':id' => 2), 'params' => array(':id' => 2),
@ -80,14 +52,60 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertEquals('user2', $customer->name); $this->assertEquals('user2', $customer->name);
// find count // find count
$this->assertEquals(3, Customer::find()->count()); // $this->assertEquals(3, Customer::count());
$this->assertEquals(3, Customer::count()); // $this->assertEquals(2, Customer::count(array(
$this->assertEquals(2, Customer::count(array( // 'where' => 'id=1 OR id=2',
'where' => 'id=1 OR id=2', // )));
))); // $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value());
$this->assertEquals(2, Customer::count()->where('id=1 OR id=2')); }
public function testFindBySql()
{
// find one
$customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one();
$this->assertTrue($customer instanceof Customer);
$this->assertEquals('user3', $customer->name);
// find all
$customers = Customer::findBySql('SELECT * FROM tbl_customer')->all();
$this->assertEquals(3, count($customers));
// find with parameter binding
$customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one();
$this->assertTrue($customer instanceof Customer);
$this->assertEquals('user2', $customer->name);
} }
public function testScope()
{
$customers = Customer::find(array(
'scopes' => array('active'),
))->all();
$this->assertEquals(2, count($customers));
$customers = Customer::find()->active()->all();
$this->assertEquals(2, count($customers));
}
//
// public function testFindLazy()
// {
// $customer = Customer::find(2);
// $orders = $customer->orders;
// $this->assertEquals(2, count($orders));
//
// $orders = $customer->orders()->where('id=3')->all();
// $this->assertEquals(1, count($orders));
// $this->assertEquals(3, $orders[0]->id);
// }
//
// public function testFindEager()
// {
// $customers = Customer::find()->with('orders')->all();
// $this->assertEquals(3, count($customers));
// $this->assertEquals(1, count($customers[0]->orders));
// $this->assertEquals(2, count($customers[1]->orders));
// }
// public function testInsert() // public function testInsert()
// { // {
// $customer = new Customer; // $customer = new Customer;

Loading…
Cancel
Save