Browse Source

new AR WIP

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
377181a008
  1. 15
      docs/internals/ar.md
  2. 27
      docs/internals/database.md
  3. 178
      docs/model.md
  4. 4
      framework/base/Model.php
  5. 19
      framework/db/ar/ActiveMetaData.php
  6. 336
      framework/db/ar/ActiveQuery.php
  7. 264
      framework/db/ar/ActiveRecord.php
  8. 11
      framework/db/dao/Connection.php
  9. 17
      tests/unit/data/ar/Customer.php
  10. 8
      tests/unit/data/ar/Item.php
  11. 50
      tests/unit/data/ar/Order.php
  12. 18
      tests/unit/data/ar/OrderItem.php
  13. 594
      tests/unit/framework/db/ar/ActiveRecordTest.php
  14. 20
      tests/unit/framework/db/dao/QueryTest.php

15
docs/internals/ar.md

@ -0,0 +1,15 @@
ActiveRecord
============
Query
-----
### Basic Queries
### Relational Queries
### Scopes

27
docs/internals/database.md

@ -0,0 +1,27 @@
Working with Database
=====================
Architecture
------------
### Data Access Object (DAO)
* Connection
* Command
* DataReader
* Transaction
### Schema
* TableSchema
* ColumnSchema
### Query Builder
* Query
* QueryBuilder
### ActiveRecord
* ActiveRecord
* ActiveQuery

178
docs/model.md

@ -1,6 +1,184 @@
Model Model
===== =====
Attributes
----------
Attributes store the actual data represented by a model and can
be accessed like object member variables. For example, a `Post` model
may contain a `title` attribute and a `content` attribute which may be
accessed as follows,
~~~php
$post->title = 'Hello, world';
$post->content = 'Something interesting is happening';
echo $post->title;
echo $post->content;
~~~
A model should list all its available attributes in the `attributes()` method.
Attributes may be implemented in various ways. The [[\yii\base\Model]] class
implements attributes as public member variables of the class, while the
[[\yii\db\ar\ActiveRecord]] class implements them as DB table columns. For example,
~~~php
// LoginForm has two attributes: username and password
class LoginForm extends \yii\base\Model
{
public $username;
public $password;
}
// Post is associated with the tbl_post DB table.
// Its attributes correspond to the columns in tbl_post
class Post extends \yii\db\ar\ActiveRecord
{
public function table()
{
return 'tbl_post';
}
}
~~~
### Attribute Labels
Scenarios
---------
A model may be used in different scenarios. For example, a `User` model may be
used to collect user login inputs, and it may also be used for user registration
purpose. For this reason, each model has a property named `scenario` which stores
the name of the scenario that the model is currently being used. As we will explain
in the next few sections, the concept of scenario is mainly used in validation and
massive attribute assignment.
Associated with each scenario is a list of attributes that are *active* in that
particular scenario. For example, in the `login` scenario, only the `username`
and `password` attributes are active; while in the `register` scenario,
additional attributes such as `email` are *active*.
Possible scenarios should be listed in the `scenarios()` method which returns an array
whose keys are the scenario names and whose values are the corresponding
active attribute lists. Below is an example:
~~~php
class User extends \yii\db\ar\ActiveRecord
{
public function table()
{
return 'tbl_user';
}
public function scenarios()
{
return array(
'login' => array('username', 'password'),
'register' => array('username', 'email', 'password'),
);
}
}
~~~
Sometimes, we want to mark that an attribute is not safe for massive assignment
(but we still want it to be validated). We may do so by prefixing an exclamation
character to the attribute name when declaring it in `scenarios()`. For example,
~~~php
array('username', 'password', '!secret')
~~~
Validation
----------
When a model is used to collect user input data via its attributes,
it usually needs to validate the affected attributes to make sure they
satisfy certain requirements, such as an attribute cannot be empty,
an attribute must contain letters only, etc. If errors are found in
validation, they may be presented to the user to help him fix the errors.
The following example shows how the validation is performed:
~~~php
$model = new LoginForm;
$model->username = $_POST['username'];
$model->password = $_POST['password'];
if ($model->validate()) {
// ...login the user...
} else {
$errors = $model->getErrors();
// ...display the errors to the end user...
}
~~~
The possible validation rules for a model should be listed in its
`rules()` method. Each validation rule applies to one or several attributes
and is effective in one or several scenarios. A rule can be specified
using a validator object - an instance of a [[\yii\validators\Validator]]
child class, or an array with the following format:
~~~php
array(
'attribute1, attribute2, ...',
'validator class or alias',
// specifies in which scenario(s) this rule is active.
// if not given, it means it is active in all scenarios
'on' => 'scenario1, scenario2, ...',
// the following name-value pairs will be used
// to initialize the validator properties...
'name1' => 'value1',
'name2' => 'value2',
....
)
~~~
When `validate()` is called, the actual validation rules executed are
determined using both of the following criteria:
* the rules must be associated with at least one active attribute;
* the rules must be active for the current scenario.
### Active Attributes
An attribute is *active* if it is subject to some validations in the current scenario.
### Safe Attributes
An attribute is *safe* if it can be massively assigned in the current scenario.
Massive Access of Attributes
----------------------------
Massive Attribute Retrieval
---------------------------
Attributes can be massively retrieved via the `attributes` property.
The following code will return *all* attributes in the `$post` model
as an array of name-value pairs.
~~~php
$attributes = $post->attributes;
var_dump($attributes);
~~~
Massive Attribute Assignment
----------------------------
Safe Attributes
---------------
Safe attributes are those that can be massively assigned. For example,
Validation rules and mass assignment Validation rules and mass assignment
------------------------------------ ------------------------------------

4
framework/base/Model.php

@ -586,7 +586,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
// use validators to determine active attributes // use validators to determine active attributes
$attributes = array(); $attributes = array();
foreach ($this->attributes() as $attribute) { foreach ($this->attributes() as $attribute) {
if ($this->getActiveValidators($attribue) !== array()) { if ($this->getActiveValidators($attribute) !== array()) {
$attributes[] = $attribute; $attributes[] = $attribute;
} }
} }
@ -614,7 +614,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
*/ */
public function offsetExists($offset) public function offsetExists($offset)
{ {
return property_exists($this, $offset) && $this->$offset !== null; return $this->$offset !== null;
} }
/** /**

19
framework/db/ar/ActiveMetaData.php

@ -26,9 +26,9 @@ class ActiveMetaData
*/ */
public $modelClass; public $modelClass;
/** /**
* @var array list of relations * @var ActiveRecord the model instance that can be used to access non-static methods
*/ */
public $relations = array(); public $model;
/** /**
* Returns an instance of ActiveMetaData for the specified model class. * Returns an instance of ActiveMetaData for the specified model class.
@ -55,21 +55,18 @@ class ActiveMetaData
public function __construct($modelClass) public function __construct($modelClass)
{ {
$this->modelClass = $modelClass; $this->modelClass = $modelClass;
$tableName = $modelClass::tableName(); $this->model = new $modelClass;
$this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); $tableName = $this->model->tableName();
$this->table = $this->model->getDbConnection()->getDriver()->getTableSchema($tableName);
if ($this->table === null) { if ($this->table === null) {
throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'.");
} }
$primaryKey = $modelClass::primaryKey(); $primaryKey = $this->model->primaryKey();
if ($primaryKey !== null) { if ($primaryKey !== $this->table->primaryKey) {
$this->table->fixPrimaryKey($primaryKey); $this->table->fixPrimaryKey($primaryKey);
} elseif ($this->table->primaryKey === null) { } elseif ($primaryKey === null) {
throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key."); throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key.");
} }
foreach ($modelClass::relations() as $name => $config) {
$this->addRelation($name, $config);
}
} }
/** /**

336
framework/db/ar/ActiveQuery.php

@ -10,10 +10,310 @@
namespace yii\db\ar; namespace yii\db\ar;
use yii\db\dao\BaseQuery;
use yii\base\VectorIterator; 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
{
/**
* @var string the name of the ActiveRecord class.
*/
public $modelClass;
/**
* @var array list of relations that this query should be performed with
*/
public $with;
/**
* @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.
*/
public $index;
/**
* @var boolean whether to return each record as an array. If false (default), an object
* of [[modelClass]] will be created to represent each record.
*/
public $asArray;
/**
* @var array list of scopes that should be applied to this query
*/
public $scopes;
/**
* @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]].
*/
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)
{
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()
{
return $this->findRecords();
}
/**
* 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()
{
$this->limit = 1;
$records = $this->findRecords();
return isset($records[0]) ? $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]);
}
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->from === null) {
$modelClass = $this->modelClass;
$tableName = $modelClass::model()->getTableSchema()->name;
$this->from = array($tableName);
}
$this->sql = $this->connection->getQueryBuilder()->build($this);
}
$command = $this->connection->createCommand($this->sql, $this->params);
$rows = $command->queryAll();
return $this->createRecords($rows);
}
protected function findWithRelations()
{
$records = $this->findRecords();
}
protected function createRecords($rows)
{
$records = array();
if ($this->asArray) {
if ($this->index === null) {
return $rows;
}
foreach ($rows as $row) {
$records[$row[$this->index]] = $row;
}
} else {
/** @var $class ActiveRecord */
$class = $this->modelClass;
if ($this->index === null) {
foreach ($rows as $row) {
$records[] = $class::create($row);
}
} else {
foreach ($rows as $row) {
$records[$row[$this->index]] = $class::create($row);
}
}
}
return $records;
}
}
/** /**
* 1. eager loading, base limited and has has_many relations * 1. eager loading, base limited and has has_many relations
* 2. * 2.
@ -24,7 +324,7 @@ use yii\db\Exception;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable class ActiveQuery2 extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable
{ {
/** /**
* @var string the SQL statement to be executed to retrieve primary records. * @var string the SQL statement to be executed to retrieve primary records.
@ -244,4 +544,38 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA
{ {
return new ActiveFinder($this->getDbConnection()); 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;
}
} }

264
framework/db/ar/ActiveRecord.php

@ -76,16 +76,6 @@ abstract class ActiveRecord extends Model
} }
/** /**
* Returns the metadata for this AR class.
* @param boolean $refresh whether to rebuild the metadata.
* @return ActiveMetaData the meta for this AR class.
*/
public static function getMetaData($refresh = false)
{
return ActiveMetaData::getInstance(get_called_class(), $refresh);
}
/**
* Returns the database connection used by this AR class. * Returns the database connection used by this AR class.
* By default, the "db" application component is used as the database connection. * By default, the "db" application component is used as the database connection.
* You may override this method if you want to use a different database connection. * You may override this method if you want to use a different database connection.
@ -114,12 +104,12 @@ abstract class ActiveRecord extends Model
* // find all active customers and order them by their age: * // find all active customers and order them by their age:
* $customers = Customer::find() * $customers = Customer::find()
* ->where(array('status' => 1)) * ->where(array('status' => 1))
* ->order('age') * ->orderBy('age')
* ->all(); * ->all();
* // or alternatively: * // or alternatively:
* $customers = Customer::find(array( * $customers = Customer::find(array(
* 'where' => array('status' => 1), * 'where' => array('status' => 1),
* 'order' => 'age', * 'orderBy' => 'age',
* ))->all(); * ))->all();
* ~~~ * ~~~
* *
@ -141,14 +131,14 @@ abstract class ActiveRecord extends Model
} }
} elseif ($q !== null) { } elseif ($q !== null) {
// query by primary key // query by primary key
$primaryKey = static::getMetaData()->table->primaryKey; $primaryKey = static::model()->primaryKey();
return $query->where(array($primaryKey[0] => $q))->one(); return $query->where(array($primaryKey[0] => $q))->one();
} }
return $query; return $query;
} }
/** /**
* Creates an [[ActiveQuery]] instance and query by a given SQL statement. * Creates an [[ActiveQuery]] instance and queries by a given SQL statement.
* Note that because the SQL statement is already specified, calling further * Note that because the SQL statement is already specified, calling further
* query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect. * query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect.
* Methods such as `with()`, `asArray()` can still be called though. * Methods such as `with()`, `asArray()` can still be called though.
@ -183,10 +173,12 @@ abstract class ActiveRecord extends Model
* echo Customer::count('COUNT(DISTINCT age)')->value(); * echo Customer::count('COUNT(DISTINCT age)')->value();
* ~~~ * ~~~
* *
* @param array $q the query configuration. This should be an array of name-value pairs. * @param array|string $q the query option. This can be one of the followings:
* It will be used to configure the [[ActiveQuery]] object for query purpose. *
* - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object.
* - a string: the count expression, e.g. 'COUNT(DISTINCT age)'.
* *
* @return integer the counting result * @return ActiveQuery the [[ActiveQuery]] instance
*/ */
public static function count($q = null) public static function count($q = null)
{ {
@ -195,11 +187,12 @@ abstract class ActiveRecord extends Model
foreach ($q as $name => $value) { foreach ($q as $name => $value) {
$query->$name = $value; $query->$name = $value;
} }
} } elseif ($q !== null) {
if ($query->select === null) { $query->select = array($q);
} elseif ($query->select === null) {
$query->select = array('COUNT(*)'); $query->select = array('COUNT(*)');
} }
return $query->value(); return $query;
} }
/** /**
@ -275,122 +268,26 @@ abstract class ActiveRecord extends Model
} }
/** /**
* Declares the primary key name for this AR class. * Returns the schema information of the DB table associated with this AR class.
* This method is meant to be overridden in case when the table has no primary key defined * @return TableSchema the schema information of the DB table associated with this AR class.
* (for some legacy database). If the table already has a primary key,
* you do not need to override this method. The default implementation simply returns null,
* meaning using the primary key defined in the database table.
* @return string|array the primary key of the associated database table.
* If the key is a single column, it should return the column name;
* If the key is a composite one consisting of several columns, it should
* return the array of the key column names.
*/ */
public function primaryKey() public function getTableSchema()
{ {
return $this->getDbConnection()->getTableSchema($this->tableName());
} }
/** /**
* Declares the relations for this AR class. * Returns the primary keys for this AR class.
* * The default implementation will return the primary keys as declared
* Child classes may override this method to specify their relations. * in the DB table that is associated with this AR class.
* * If the DB table does not declare any primary key, you should override
* The following code shows how to declare relations for a `Programmer` AR class: * this method to return the attributes that you want to use as primary keys
* * for this AR class.
* ~~~ * @return string[] the primary keys of the associated database table.
* return array(
* 'manager:Manager' => '@.id = ?.manager_id',
* 'assignments:Assignment[]' => array(
* 'on' => '@.owner_id = ?.id AND @.status = 1',
* 'order' => '@.create_time DESC',
* ),
* 'projects:Project[]' => array(
* 'via' => 'assignments',
* 'on' => '@.id = ?.project_id',
* ),
* );
* ~~~
*
* This method should be overridden to declare related objects.
*
* There are four types of relations that may exist between two active record objects:
* <ul>
* <li>BELONGS_TO: e.g. a member belongs to a team;</li>
* <li>HAS_ONE: e.g. a member has at most one profile;</li>
* <li>HAS_MANY: e.g. a team has many members;</li>
* <li>MANY_MANY: e.g. a member has many skills and a skill belongs to a member.</li>
* </ul>
*
* Besides the above relation types, a special relation called STAT is also supported
* that can be used to perform statistical query (or aggregational query).
* It retrieves the aggregational information about the related objects, such as the number
* of comments for each post, the average rating for each product, etc.
*
* Each kind of related objects is defined in this method as an array with the following elements:
* <pre>
* 'varName'=>array('relationType', 'className', 'foreign_key', ...additional options)
* </pre>
* where 'varName' refers to the name of the variable/property that the related object(s) can
* be accessed through; 'relationType' refers to the type of the relation, which can be one of the
* following four constants: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY;
* 'className' refers to the name of the active record class that the related object(s) is of;
* and 'foreign_key' states the foreign key that relates the two kinds of active record.
* Note, for composite foreign keys, they must be listed together, separated by commas;
* and for foreign keys used in MANY_MANY relation, the joining table must be declared as well
* (e.g. 'join_table(fk1, fk2)').
*
* Additional options may be specified as name-value pairs in the rest array elements:
* <ul>
* <li>'select': string|array, a list of columns to be selected. Defaults to '*', meaning all columns.
* Column names should be disambiguated if they appear in an expression (e.g. COUNT(relationName.name) AS name_count).</li>
* <li>'condition': string, the WHERE clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age&gt;20)</li>
* <li>'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age DESC)</li>
* <li>'with': string|array, a list of child related objects that should be loaded together with this object.
* Note, this is only honored by lazy loading, not eager loading.</li>
* <li>'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.</li>
* <li>'alias': the alias for the table associated with this relationship.
* This option has been available since version 1.0.1. It defaults to null,
* meaning the table alias is the same as the relation name.</li>
* <li>'params': the parameters to be bound to the generated SQL statement.
* This should be given as an array of name-value pairs. This option has been
* available since version 1.0.3.</li>
* <li>'on': the ON clause. The condition specified here will be appended
* to the joining condition using the AND operator. This option has been
* available since version 1.0.2.</li>
* <li>'index': the name of the column whose values should be used as keys
* of the array that stores related objects. This option is only available to
* HAS_MANY and MANY_MANY relations. This option has been available since version 1.0.7.</li>
* <li>'scopes': scopes to apply. In case of a single scope can be used like 'scopes'=>'scopeName',
* in case of multiple scopes can be used like 'scopes'=>array('scopeName1','scopeName2').
* This option has been available since version 1.1.9.</li>
* </ul>
*
* The following options are available for certain relations when lazy loading:
* <ul>
* <li>'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.</li>
* <li>'having': string, the HAVING clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.</li>
* <li>'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.</li>
* <li>'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.</li>
* <li>'through': name of the model's relation that will be used as a bridge when getting related data. Can be set only for HAS_ONE and HAS_MANY. This option has been available since version 1.1.7.</li>
* </ul>
*
* Below is an example declaring related objects for 'Post' active record class:
* <pre>
* return array(
* 'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
* 'comments'=>array(self::HAS_MANY, 'Comment', 'post_id', 'with'=>'author', 'order'=>'create_time DESC'),
* 'tags'=>array(self::MANY_MANY, 'Tag', 'post_tag(post_id, tag_id)', 'order'=>'name'),
* );
* </pre>
*
* @return array list of related object declarations. Defaults to empty array.
*/ */
public static function relations() public function primaryKey()
{ {
return array(); return $this->getTableSchema()->primaryKey;
} }
/** /**
@ -408,42 +305,6 @@ abstract class ActiveRecord extends Model
} }
/** /**
* Returns the declaration of named scopes.
* A named scope represents a query criteria that can be chained together with
* other named scopes and applied to a query. This method should be overridden
* by child classes to declare named scopes for the particular AR classes.
* For example, the following code declares two named scopes: 'recently' and
* 'published'.
* <pre>
* return array(
* 'published'=>array(
* 'condition'=>'status=1',
* ),
* 'recently'=>array(
* 'order'=>'create_time DESC',
* 'limit'=>5,
* ),
* );
* </pre>
* If the above scopes are declared in a 'Post' model, we can perform the following
* queries:
* <pre>
* $posts=Post::model()->published()->findAll();
* $posts=Post::model()->published()->recently()->findAll();
* $posts=Post::model()->published()->with('comments')->findAll();
* </pre>
* Note that the last query is a relational query.
*
* @return array the scope definition. The array keys are scope names; the array
* values are the corresponding scope definitions. Each scope definition is represented
* as an array whose keys must be properties of {@link CDbCriteria}.
*/
public static function scopes()
{
return array();
}
/**
* PHP getter magic method. * PHP getter magic method.
* This method is overridden so that attributes and related objects can be accessed like properties. * This method is overridden so that attributes and related objects can be accessed like properties.
* @param string $name property name * @param string $name property name
@ -455,13 +316,13 @@ abstract class ActiveRecord extends Model
if (isset($this->_attributes[$name])) { if (isset($this->_attributes[$name])) {
return $this->_attributes[$name]; return $this->_attributes[$name];
} }
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
return null; return null;
} elseif (isset($md->relations[$name])) { } elseif (method_exists($this, $name)) {
if (isset($this->_related[$name]) || $this->_related !== null && array_key_exists($name, $this->_related)) { if (isset($this->_related[$name]) || $this->_related !== null && array_key_exists($name, $this->_related)) {
return $this->_related[$name]; return $this->_related[$name];
} else { } else {
// todo
return $this->_related[$name] = $this->findByRelation($md->relations[$name]); return $this->_related[$name] = $this->findByRelation($md->relations[$name]);
} }
} }
@ -476,10 +337,9 @@ abstract class ActiveRecord extends Model
*/ */
public function __set($name, $value) public function __set($name, $value)
{ {
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
$this->_attributes[$name] = $value; $this->_attributes[$name] = $value;
} elseif (isset($md->relations[$name])) { } elseif (method_exists($this, $name)) {
$this->_related[$name] = $value; $this->_related[$name] = $value;
} else { } else {
parent::__set($name, $value); parent::__set($name, $value);
@ -498,8 +358,7 @@ abstract class ActiveRecord extends Model
if (isset($this->_attributes[$name]) || isset($this->_related[$name])) { if (isset($this->_attributes[$name]) || isset($this->_related[$name])) {
return true; return true;
} }
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) {
if (isset($md->table->columns[$name]) || isset($md->relations[$name])) {
return false; return false;
} else { } else {
return parent::__isset($name); return parent::__isset($name);
@ -514,10 +373,9 @@ abstract class ActiveRecord extends Model
*/ */
public function __unset($name) public function __unset($name)
{ {
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
unset($this->_attributes[$name]); unset($this->_attributes[$name]);
} elseif (isset($md->relations[$name])) { } elseif (method_exists($this, $name)) {
unset($this->_related[$name]); unset($this->_related[$name]);
} else { } else {
parent::__unset($name); parent::__unset($name);
@ -525,23 +383,6 @@ abstract class ActiveRecord extends Model
} }
/** /**
* Calls the named method which is not a class method.
* Do not call this method. This is a PHP magic method that we override
* to implement the named scope feature.
* @param string $name the method name
* @param array $params method parameters
* @return mixed the method return value
*/
public function __call($name, $params)
{
$md = $this->getMetaData();
if (isset($md->relations[$name])) {
return $this->findByRelation($md->relations[$name], isset($params[0]) ? $params[0] : array());
}
return parent::__call($name, $params);
}
/**
* 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.
* @param ActiveRelation $relation the relation object * @param ActiveRelation $relation the relation object
@ -609,19 +450,7 @@ abstract class ActiveRecord extends Model
*/ */
public function attributes() public function attributes()
{ {
return array_keys($this->getMetaData()->table->columns); return array_keys($this->getTableSchema()->columns);
}
/**
* Returns a list of scenarios and the corresponding relevant attributes.
* Please refer to [[\yii\base\Model::scenarios()]] for more details.
* The implementation here simply returns an empty array. You may override
* this method to return the scenarios that you want to use with this AR class.
* @return array a list of scenarios and the corresponding relevant attributes.
*/
public function scenarios()
{
return array();
} }
/** /**
@ -726,7 +555,7 @@ abstract class ActiveRecord extends Model
$db = $this->getDbConnection(); $db = $this->getDbConnection();
$command = $query->insert($this->tableName(), $values)->createCommand($db); $command = $query->insert($this->tableName(), $values)->createCommand($db);
if ($command->execute()) { if ($command->execute()) {
$table = $this->getMetaData()->table; $table = $this->getTableSchema();
if ($table->sequenceName !== null) { if ($table->sequenceName !== null) {
foreach ($table->primaryKey as $name) { foreach ($table->primaryKey as $name) {
if (!isset($this->_attributes[$name])) { if (!isset($this->_attributes[$name])) {
@ -973,12 +802,12 @@ abstract class ActiveRecord extends Model
*/ */
public function getPrimaryKey($asArray = false) public function getPrimaryKey($asArray = false)
{ {
$table = static::getMetaData()->table; $keys = $this->primaryKey();
if (count($table->primaryKey) === 1 && !$asArray) { if (count($keys) === 1 && !$asArray) {
return isset($this->_attributes[$table->primaryKey[0]]) ? $this->_attributes[$table->primaryKey[0]] : null; return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
} else { } else {
$values = array(); $values = array();
foreach ($table->primaryKey as $name) { foreach ($keys as $name) {
$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
} }
return $values; return $values;
@ -998,12 +827,12 @@ abstract class ActiveRecord extends Model
*/ */
public function getOldPrimaryKey($asArray = false) public function getOldPrimaryKey($asArray = false)
{ {
$table = static::getMetaData()->table; $keys = $this->primaryKey();
if (count($table->primaryKey) === 1 && !$asArray) { if (count($keys) === 1 && !$asArray) {
return isset($this->_oldAttributes[$table->primaryKey[0]]) ? $this->_oldAttributes[$table->primaryKey[0]] : null; return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
} else { } else {
$values = array(); $values = array();
foreach ($table->primaryKey as $name) { foreach ($keys as $name) {
$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
} }
return $values; return $values;
@ -1018,7 +847,7 @@ abstract class ActiveRecord extends Model
public static function create($row) public static function create($row)
{ {
$record = static::instantiate($row); $record = static::instantiate($row);
$columns = static::getMetaData()->table->columns; $columns = static::model()->getTableSchema()->columns;
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;
@ -1042,8 +871,7 @@ abstract class ActiveRecord extends Model
*/ */
public static function instantiate($row) public static function instantiate($row)
{ {
$class = get_called_class(); return new static;
return new $class;
} }
/** /**

11
framework/db/dao/Connection.php

@ -461,6 +461,17 @@ class Connection extends \yii\base\ApplicationComponent
} }
/** /**
* Obtains the metadata for the named table.
* @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
* @param boolean $refresh whether to reload the table schema even if it is found in the cache.
* @return TableSchema table metadata. Null if the named table does not exist.
*/
public function getTableSchema($name, $refresh = false)
{
return $this->getDriver()->getTableSchema($name, $refresh);
}
/**
* Returns the ID of the last inserted row or sequence value. * Returns the ID of the last inserted row or sequence value.
* @param string $sequenceName name of the sequence object (required by some DBMS) * @param string $sequenceName name of the sequence object (required by some DBMS)
* @return string the row ID of the last row inserted, or the last value retrieved from the sequence object * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object

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

@ -1,28 +1,29 @@
<?php <?php
namespace yiiunit\data\ar; namespace yiiunit\data\ar;
use yii\db\ar\ActiveQuery;
class Customer extends ActiveRecord class Customer extends ActiveRecord
{ {
const STATUS_ACTIVE = 1; const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 2; const STATUS_INACTIVE = 2;
public static function tableName() public function tableName()
{ {
return 'tbl_customer'; return 'tbl_customer';
} }
public static function relations() public function orders()
{ {
return array( return $this->hasMany('Order', array('customer_id' => 'id'));
'orders:Order[]' => array(
'link' => array('customer_id' => 'id'),
),
);
} }
/**
* @param ActiveQuery $query
* @return ActiveQuery
*/
public function active($query) public function active($query)
{ {
return $query->andWhere('@.`status` = ' . self::STATUS_ACTIVE); return $query->andWhere('`status` = ' . self::STATUS_ACTIVE);
} }
} }

8
tests/unit/data/ar/Item.php

@ -4,14 +4,8 @@ namespace yiiunit\data\ar;
class Item extends ActiveRecord class Item extends ActiveRecord
{ {
public static function tableName() public function tableName()
{ {
return 'tbl_item'; return 'tbl_item';
} }
public static function relations()
{
return array(
);
}
} }

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

@ -4,40 +4,30 @@ namespace yiiunit\data\ar;
class Order extends ActiveRecord class Order extends ActiveRecord
{ {
public static function tableName() public function tableName()
{ {
return 'tbl_order'; return 'tbl_order';
} }
public static function relations() public function customer()
{ {
return array( return $this->hasOne('Customer', array('id' => 'customer_id'));
'customer:Customer' => array( }
'link' => array('id' => 'customer_id'),
), public function orderItems()
'orderItems:OrderItem' => array( {
'link' => array('order_id' => 'id'), return $this->hasMany('OrderItem', array('order_id' => 'id'));
), }
'items:Item[]' => array(
'via' => 'orderItems', public function items()
'link' => array( {
'id' => 'item_id', return $this->hasMany('Item', array('id' => 'item_id'))
), ->via('orderItems')->orderBy('id');
'order' => '@.id', }
),
'books:Item[]' => array( public function books()
'joinType' => 'INNER JOIN', {
'via' => array( return $this->manyMany('Item', array('id' => 'item_id'), 'tbl_order_item', array('item_id', 'id'))
'table' => 'tbl_order_item', ->where('category_id = 1');
'link' => array(
'order_id' => 'id',
),
),
'link' => array(
'id' => 'item_id',
),
'on' => '@.category_id = 1',
),
);
} }
} }

18
tests/unit/data/ar/OrderItem.php

@ -4,20 +4,18 @@ namespace yiiunit\data\ar;
class OrderItem extends ActiveRecord class OrderItem extends ActiveRecord
{ {
public static function tableName() public function tableName()
{ {
return 'tbl_order_item'; return 'tbl_order_item';
} }
public static function relations() public function order()
{ {
return array( return $this->hasOne('Order', array('id' => 'order_id'));
'order:Order' => array( }
'link' => array('order_id' => 'id'),
), public function item()
'item:Item' => array( {
'link' => array('item_id' => 'id'), return $this->hasOne('Item', array('id' => 'item_id'));
),
);
} }
} }

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

@ -16,87 +16,6 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
ActiveRecord::$db = $this->getConnection(); ActiveRecord::$db = $this->getConnection();
} }
public function testInsert()
{
$customer = new Customer;
$customer->email = 'user4@example.com';
$customer->name = 'user4';
$customer->address = 'address4';
$this->assertNull($customer->id);
$this->assertTrue($customer->isNewRecord);
$customer->save();
$this->assertEquals(4, $customer->id);
$this->assertFalse($customer->isNewRecord);
}
public function testUpdate()
{
// save
$customer = Customer::find(2);
$this->assertTrue($customer instanceof Customer);
$this->assertEquals('user2', $customer->name);
$this->assertFalse($customer->isNewRecord);
$customer->name = 'user2x';
$customer->save();
$this->assertEquals('user2x', $customer->name);
$this->assertFalse($customer->isNewRecord);
$customer2 = Customer::find(2);
$this->assertEquals('user2x', $customer2->name);
// updateCounters
$pk = array('order_id' => 2, 'item_id' => 4);
$orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(1, $orderItem->quantity);
$ret = $orderItem->updateCounters(array('quantity' => -1));
$this->assertTrue($ret);
$this->assertEquals(0, $orderItem->quantity);
$orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(0, $orderItem->quantity);
// updateAll
$customer = Customer::find(3);
$this->assertEquals('user3', $customer->name);
$ret = Customer::updateAll(array(
'name' => 'temp',
), array('id' => 3));
$this->assertEquals(1, $ret);
$customer = Customer::find(3);
$this->assertEquals('temp', $customer->name);
// updateCounters
$pk = array('order_id' => 1, 'item_id' => 2);
$orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(2, $orderItem->quantity);
$ret = OrderItem::updateAllCounters(array(
'quantity' => 3,
), $pk);
$this->assertEquals(1, $ret);
$orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(5, $orderItem->quantity);
}
public function testDelete()
{
// delete
$customer = Customer::find(2);
$this->assertTrue($customer instanceof Customer);
$this->assertEquals('user2', $customer->name);
$customer->delete();
$customer = Customer::find(2);
$this->assertNull($customer);
// deleteAll
$customers = Customer::find()->all();
$this->assertEquals(2, count($customers));
$ret = Customer::deleteAll();
$this->assertEquals(2, $ret);
$customers = Customer::find()->all();
$this->assertEquals(0, count($customers));
}
public function testFind() public function testFind()
{ {
// find one // find one
@ -105,6 +24,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$customer = $result->one(); $customer = $result->one();
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(1, $result->count); $this->assertEquals(1, $result->count);
$this->assertEquals(1, count($result));
// find all // find all
$result = Customer::find(); $result = Customer::find();
@ -138,6 +58,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($result[0] instanceof Customer); $this->assertTrue($result[0] instanceof Customer);
$this->assertTrue($result[1] instanceof Customer); $this->assertTrue($result[1] instanceof Customer);
$this->assertTrue($result[2] 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);
@ -164,186 +85,337 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$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::count()->where('id=1 OR id=2'));
} }
public function testFindBySql() // public function testInsert()
{ // {
// find one // $customer = new Customer;
$customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); // $customer->email = 'user4@example.com';
$this->assertTrue($customer instanceof Customer); // $customer->name = 'user4';
$this->assertEquals('user3', $customer->name); // $customer->address = 'address4';
//
// find all // $this->assertNull($customer->id);
$customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); // $this->assertTrue($customer->isNewRecord);
$this->assertEquals(3, count($customers)); //
// $customer->save();
// find with parameter binding //
$customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); // $this->assertEquals(4, $customer->id);
$this->assertTrue($customer instanceof Customer); // $this->assertFalse($customer->isNewRecord);
$this->assertEquals('user2', $customer->name); // }
//
// count // public function testUpdate()
$query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); // {
$query->one(); // // save
$this->assertEquals(3, $query->count); // $customer = Customer::find(2);
$query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, $query->count); // $this->assertEquals('user2', $customer->name);
} // $this->assertFalse($customer->isNewRecord);
// $customer->name = 'user2x';
public function testQueryMethods() // $customer->save();
{ // $this->assertEquals('user2x', $customer->name);
$customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); // $this->assertFalse($customer->isNewRecord);
$this->assertTrue($customer instanceof Customer); // $customer2 = Customer::find(2);
$this->assertEquals('user2', $customer->name); // $this->assertEquals('user2x', $customer2->name);
//
$customer = Customer::find()->where(array('name' => 'user3'))->one(); // // updateCounters
$this->assertTrue($customer instanceof Customer); // $pk = array('order_id' => 2, 'item_id' => 4);
$this->assertEquals('user3', $customer->name); // $orderItem = OrderItem::find()->where($pk)->one();
// $this->assertEquals(1, $orderItem->quantity);
$customer = Customer::find()->select('id')->order('id DESC')->one(); // $ret = $orderItem->updateCounters(array('quantity' => -1));
$this->assertTrue($customer instanceof Customer); // $this->assertTrue($ret);
$this->assertEquals(3, $customer->id); // $this->assertEquals(0, $orderItem->quantity);
$this->assertEquals(null, $customer->name); // $orderItem = OrderItem::find()->where($pk)->one();
// $this->assertEquals(0, $orderItem->quantity);
// scopes //
$customers = Customer::find()->active()->all(); // // updateAll
$this->assertEquals(2, count($customers)); // $customer = Customer::find(3);
$customers = Customer::find(array( // $this->assertEquals('user3', $customer->name);
'scopes' => array('active'), // $ret = Customer::updateAll(array(
))->all(); // 'name' => 'temp',
$this->assertEquals(2, count($customers)); // ), array('id' => 3));
// $this->assertEquals(1, $ret);
// asArray // $customer = Customer::find(3);
$customers = Customer::find()->order('id')->asArray()->all(); // $this->assertEquals('temp', $customer->name);
$this->assertEquals('user2', $customers[1]['name']); //
// // updateCounters
// index // $pk = array('order_id' => 1, 'item_id' => 2);
$customers = Customer::find()->order('id')->index('name')->all(); // $orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(2, $customers['user2']['id']); // $this->assertEquals(2, $orderItem->quantity);
} // $ret = OrderItem::updateAllCounters(array(
// 'quantity' => 3,
public function testEagerLoading() // ), $pk);
{ // $this->assertEquals(1, $ret);
// has many // $orderItem = OrderItem::find()->where($pk)->one();
$customers = Customer::find()->with('orders')->order('@.id')->all(); // $this->assertEquals(5, $orderItem->quantity);
$this->assertEquals(3, count($customers)); // }
$this->assertEquals(1, count($customers[0]->orders)); //
$this->assertEquals(2, count($customers[1]->orders)); // public function testDelete()
$this->assertEquals(0, count($customers[2]->orders)); // {
// // delete
// nested // $customer = Customer::find(2);
$customers = Customer::find()->with('orders.customer')->order('@.id')->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($customers)); // $this->assertEquals('user2', $customer->name);
$this->assertEquals(1, $customers[0]->orders[0]->customer->id); // $customer->delete();
$this->assertEquals(2, $customers[1]->orders[0]->customer->id); // $customer = Customer::find(2);
$this->assertEquals(2, $customers[1]->orders[1]->customer->id); // $this->assertNull($customer);
//
// has many via relation // // deleteAll
$orders = Order::find()->with('items')->order('@.id')->all(); // $customers = Customer::find()->all();
$this->assertEquals(3, count($orders)); // $this->assertEquals(2, count($customers));
$this->assertEquals(1, $orders[0]->items[0]->id); // $ret = Customer::deleteAll();
$this->assertEquals(2, $orders[0]->items[1]->id); // $this->assertEquals(2, $ret);
$this->assertEquals(3, $orders[1]->items[0]->id); // $customers = Customer::find()->all();
$this->assertEquals(4, $orders[1]->items[1]->id); // $this->assertEquals(0, count($customers));
$this->assertEquals(5, $orders[1]->items[2]->id); // }
//
// has many via join table // public function testFind()
$orders = Order::find()->with('books')->order('@.id')->all(); // {
$this->assertEquals(2, count($orders)); // // find one
$this->assertEquals(1, $orders[0]->books[0]->id); // $result = Customer::find();
$this->assertEquals(2, $orders[0]->books[1]->id); // $this->assertTrue($result instanceof ActiveQuery);
$this->assertEquals(2, $orders[1]->books[0]->id); // $customer = $result->one();
// $this->assertTrue($customer instanceof Customer);
// has many and base limited // $this->assertEquals(1, $result->count);
$orders = Order::find()->with('items')->order('@.id')->limit(2)->all(); //
$this->assertEquals(2, count($orders)); // // find all
$this->assertEquals(1, $orders[0]->items[0]->id); // $result = Customer::find();
// $customers = $result->all();
/// customize "with" query // $this->assertTrue(is_array($customers));
$orders = Order::find()->with(array('items' => function($q) { // $this->assertEquals(3, count($customers));
$q->order('@.id DESC'); // $this->assertTrue($customers[0] instanceof Customer);
}))->order('@.id')->limit(2)->all(); // $this->assertTrue($customers[1] instanceof Customer);
$this->assertEquals(2, count($orders)); // $this->assertTrue($customers[2] instanceof Customer);
$this->assertEquals(2, $orders[0]->items[0]->id); // $this->assertEquals(3, $result->count);
// $this->assertEquals(3, count($result));
// findBySql with //
$orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); // // check count first
$this->assertEquals(2, count($orders)); // $result = Customer::find();
// $this->assertEquals(3, $result->count);
// index and array // $customer = $result->one();
$customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($customers)); // $this->assertEquals(3, $result->count);
$this->assertTrue(isset($customers[1], $customers[2], $customers[3])); //
$this->assertTrue(is_array($customers[1])); // // iterator
$this->assertEquals(1, count($customers[1]['orders'])); // $result = Customer::find();
$this->assertEquals(2, count($customers[2]['orders'])); // $count = 0;
$this->assertEquals(0, count($customers[3]['orders'])); // foreach ($result as $customer) {
$this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); // $this->assertTrue($customer instanceof Customer);
// $count++;
// count with // }
$this->assertEquals(3, Order::count()); // $this->assertEquals($count, $result->count);
$value = Order::count(array( //
'select' => array('COUNT(DISTINCT @.id, @.customer_id)'), // // array access
'with' => 'books', // $result = Customer::find();
)); // $this->assertTrue($result[0] instanceof Customer);
$this->assertEquals(2, $value); // $this->assertTrue($result[1] instanceof Customer);
// $this->assertTrue($result[2] instanceof Customer);
} //
// // find by a single primary key
public function testLazyLoading() // $customer = Customer::find(2);
{ // $this->assertTrue($customer instanceof Customer);
// has one // $this->assertEquals('user2', $customer->name);
$order = Order::find(3); //
$this->assertTrue($order->customer instanceof Customer); // // find by attributes
$this->assertEquals(2, $order->customer->id); // $customer = Customer::find()->where(array('name' => 'user2'))->one();
// $this->assertTrue($customer instanceof Customer);
// has many // $this->assertEquals(2, $customer->id);
$customer = Customer::find(2); //
$orders = $customer->orders; // // find by Query
$this->assertEquals(2, count($orders)); // $query = array(
$this->assertEquals(2, $orders[0]->id); // 'where' => 'id=:id',
$this->assertEquals(3, $orders[1]->id); // 'params' => array(':id' => 2),
// );
// has many via join table // $customer = Customer::find($query)->one();
$orders = Order::find()->order('@.id')->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($orders)); // $this->assertEquals('user2', $customer->name);
$this->assertEquals(2, count($orders[0]->books)); //
$this->assertEquals(1, $orders[0]->books[0]->id); // // find count
$this->assertEquals(2, $orders[0]->books[1]->id); // $this->assertEquals(3, Customer::find()->count());
$this->assertEquals(array(), $orders[1]->books); // $this->assertEquals(3, Customer::count());
$this->assertEquals(1, count($orders[2]->books)); // $this->assertEquals(2, Customer::count(array(
$this->assertEquals(2, $orders[2]->books[0]->id); // 'where' => 'id=1 OR id=2',
// )));
// customized relation query // }
$customer = Customer::find(2); //
$orders = $customer->orders(array( // public function testFindBySql()
'where' => '@.id = 3', // {
)); // // find one
$this->assertEquals(1, count($orders)); // $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one();
$this->assertEquals(3, $orders[0]->id); // $this->assertTrue($customer instanceof Customer);
// $this->assertEquals('user3', $customer->name);
// original results are kept after customized query //
$orders = $customer->orders; // // find all
$this->assertEquals(2, count($orders)); // $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all();
$this->assertEquals(2, $orders[0]->id); // $this->assertEquals(3, count($customers));
$this->assertEquals(3, $orders[1]->id); //
// // find with parameter binding
// as array // $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one();
$orders = $customer->orders(array( // $this->assertTrue($customer instanceof Customer);
'asArray' => true, // $this->assertEquals('user2', $customer->name);
)); //
$this->assertEquals(2, count($orders)); // // count
$this->assertTrue(is_array($orders[0])); // $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC');
$this->assertEquals(2, $orders[0]['id']); // $query->one();
$this->assertEquals(3, $orders[1]['id']); // $this->assertEquals(3, $query->count);
// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC');
// using anonymous function to customize query condition // $this->assertEquals(3, $query->count);
$orders = $customer->orders(function($q) { // }
$q->order('@.id DESC')->asArray(); //
}); // public function testQueryMethods()
$this->assertEquals(2, count($orders)); // {
$this->assertTrue(is_array($orders[0])); // $customer = Customer::find()->where('id=:id', array(':id' => 2))->one();
$this->assertEquals(3, $orders[0]['id']); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(2, $orders[1]['id']); // $this->assertEquals('user2', $customer->name);
} //
// $customer = Customer::find()->where(array('name' => 'user3'))->one();
// $this->assertTrue($customer instanceof Customer);
// $this->assertEquals('user3', $customer->name);
//
// $customer = Customer::find()->select('id')->orderBy('id DESC')->one();
// $this->assertTrue($customer instanceof Customer);
// $this->assertEquals(3, $customer->id);
// $this->assertEquals(null, $customer->name);
//
// // scopes
// $customers = Customer::find()->active()->all();
// $this->assertEquals(2, count($customers));
// $customers = Customer::find(array(
// 'scopes' => array('active'),
// ))->all();
// $this->assertEquals(2, count($customers));
//
// // asArray
// $customers = Customer::find()->orderBy('id')->asArray()->all();
// $this->assertEquals('user2', $customers[1]['name']);
//
// // index
// $customers = Customer::find()->orderBy('id')->index('name')->all();
// $this->assertEquals(2, $customers['user2']['id']);
// }
//
// public function testEagerLoading()
// {
// // has many
// $customers = Customer::find()->with('orders')->orderBy('@.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));
//
// // nested
// $customers = Customer::find()->with('orders.customer')->orderBy('@.id')->all();
// $this->assertEquals(3, count($customers));
// $this->assertEquals(1, $customers[0]->orders[0]->customer->id);
// $this->assertEquals(2, $customers[1]->orders[0]->customer->id);
// $this->assertEquals(2, $customers[1]->orders[1]->customer->id);
//
// // has many via relation
// $orders = Order::find()->with('items')->orderBy('@.id')->all();
// $this->assertEquals(3, count($orders));
// $this->assertEquals(1, $orders[0]->items[0]->id);
// $this->assertEquals(2, $orders[0]->items[1]->id);
// $this->assertEquals(3, $orders[1]->items[0]->id);
// $this->assertEquals(4, $orders[1]->items[1]->id);
// $this->assertEquals(5, $orders[1]->items[2]->id);
//
// // has many via join table
// $orders = Order::find()->with('books')->orderBy('@.id')->all();
// $this->assertEquals(2, count($orders));
// $this->assertEquals(1, $orders[0]->books[0]->id);
// $this->assertEquals(2, $orders[0]->books[1]->id);
// $this->assertEquals(2, $orders[1]->books[0]->id);
//
// // has many and base limited
// $orders = Order::find()->with('items')->orderBy('@.id')->limit(2)->all();
// $this->assertEquals(2, count($orders));
// $this->assertEquals(1, $orders[0]->items[0]->id);
//
// /// customize "with" query
// $orders = Order::find()->with(array('items' => function($q) {
// $q->orderBy('@.id DESC');
// }))->orderBy('@.id')->limit(2)->all();
// $this->assertEquals(2, count($orders));
// $this->assertEquals(2, $orders[0]->items[0]->id);
//
// // findBySql with
// $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all();
// $this->assertEquals(2, count($orders));
//
// // index and array
// $customers = Customer::find()->with('orders.customer')->orderBy('@.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']));
//
// // 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()
// {
// // has one
// $order = Order::find(3);
// $this->assertTrue($order->customer instanceof Customer);
// $this->assertEquals(2, $order->customer->id);
//
// // has many
// $customer = Customer::find(2);
// $orders = $customer->orders;
// $this->assertEquals(2, count($orders));
// $this->assertEquals(2, $orders[0]->id);
// $this->assertEquals(3, $orders[1]->id);
//
// // has many via join table
// $orders = Order::find()->orderBy('@.id')->all();
// $this->assertEquals(3, count($orders));
// $this->assertEquals(2, count($orders[0]->books));
// $this->assertEquals(1, $orders[0]->books[0]->id);
// $this->assertEquals(2, $orders[0]->books[1]->id);
// $this->assertEquals(array(), $orders[1]->books);
// $this->assertEquals(1, count($orders[2]->books));
// $this->assertEquals(2, $orders[2]->books[0]->id);
//
// // customized relation query
// $customer = Customer::find(2);
// $orders = $customer->orders(array(
// 'where' => '@.id = 3',
// ));
// $this->assertEquals(1, count($orders));
// $this->assertEquals(3, $orders[0]->id);
//
// // original results are kept after customized query
// $orders = $customer->orders;
// $this->assertEquals(2, count($orders));
// $this->assertEquals(2, $orders[0]->id);
// $this->assertEquals(3, $orders[1]->id);
//
// // as array
// $orders = $customer->orders(array(
// 'asArray' => true,
// ));
// $this->assertEquals(2, count($orders));
// $this->assertTrue(is_array($orders[0]));
// $this->assertEquals(2, $orders[0]['id']);
// $this->assertEquals(3, $orders[1]['id']);
//
// // using anonymous function to customize query condition
// $orders = $customer->orders(function($q) {
// $q->orderBy('@.id DESC')->asArray();
// });
// $this->assertEquals(2, count($orders));
// $this->assertTrue(is_array($orders[0]));
// $this->assertEquals(3, $orders[0]['id']);
// $this->assertEquals(2, $orders[1]['id']);
// }
} }

20
tests/unit/framework/db/dao/QueryTest.php

@ -56,14 +56,14 @@ class QueryTest extends \yiiunit\MysqlTestCase
function testGroup() function testGroup()
{ {
$query = new Query; $query = new Query;
$query->group('team'); $query->groupBy('team');
$this->assertEquals('team', $query->group); $this->assertEquals('team', $query->groupBy);
$query->addGroup('company'); $query->addGroup('company');
$this->assertEquals(array('team', 'company'), $query->group); $this->assertEquals(array('team', 'company'), $query->groupBy);
$query->addGroup('age'); $query->addGroup('age');
$this->assertEquals(array('team', 'company', 'age'), $query->group); $this->assertEquals(array('team', 'company', 'age'), $query->groupBy);
} }
function testHaving() function testHaving()
@ -85,14 +85,14 @@ class QueryTest extends \yiiunit\MysqlTestCase
function testOrder() function testOrder()
{ {
$query = new Query; $query = new Query;
$query->order('team'); $query->orderBy('team');
$this->assertEquals('team', $query->order); $this->assertEquals('team', $query->orderBy);
$query->addOrder('company'); $query->addOrderBy('company');
$this->assertEquals(array('team', 'company'), $query->order); $this->assertEquals(array('team', 'company'), $query->orderBy);
$query->addOrder('age'); $query->addOrderBy('age');
$this->assertEquals(array('team', 'company', 'age'), $query->order); $this->assertEquals(array('team', 'company', 'age'), $query->orderBy);
} }
function testLimitOffset() function testLimitOffset()

Loading…
Cancel
Save