From 377181a0082d4f378bc49b17638dd71185cf9556 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 12 Nov 2012 20:31:23 -0500 Subject: [PATCH] new AR WIP --- docs/internals/ar.md | 15 + docs/internals/database.md | 27 ++ docs/model.md | 178 +++++++ framework/base/Model.php | 4 +- framework/db/ar/ActiveMetaData.php | 19 +- framework/db/ar/ActiveQuery.php | 336 +++++++++++++- framework/db/ar/ActiveRecord.php | 264 ++--------- framework/db/dao/Connection.php | 11 + tests/unit/data/ar/Customer.php | 17 +- tests/unit/data/ar/Item.php | 8 +- tests/unit/data/ar/Order.php | 50 +- tests/unit/data/ar/OrderItem.php | 18 +- tests/unit/framework/db/ar/ActiveRecordTest.php | 594 +++++++++++++----------- tests/unit/framework/db/dao/QueryTest.php | 20 +- 14 files changed, 1003 insertions(+), 558 deletions(-) diff --git a/docs/internals/ar.md b/docs/internals/ar.md index e69de29..c493269 100644 --- a/docs/internals/ar.md +++ b/docs/internals/ar.md @@ -0,0 +1,15 @@ +ActiveRecord +============ + +Query +----- + +### Basic Queries + + + +### Relational Queries + +### Scopes + + diff --git a/docs/internals/database.md b/docs/internals/database.md index e69de29..c137035 100644 --- a/docs/internals/database.md +++ b/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 \ No newline at end of file diff --git a/docs/model.md b/docs/model.md index ca823ff..e9e0d62 100644 --- a/docs/model.md +++ b/docs/model.md @@ -1,6 +1,184 @@ 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 ------------------------------------ diff --git a/framework/base/Model.php b/framework/base/Model.php index 8756b5c..731c4f6 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -586,7 +586,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess // use validators to determine active attributes $attributes = array(); foreach ($this->attributes() as $attribute) { - if ($this->getActiveValidators($attribue) !== array()) { + if ($this->getActiveValidators($attribute) !== array()) { $attributes[] = $attribute; } } @@ -614,7 +614,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function offsetExists($offset) { - return property_exists($this, $offset) && $this->$offset !== null; + return $this->$offset !== null; } /** diff --git a/framework/db/ar/ActiveMetaData.php b/framework/db/ar/ActiveMetaData.php index 74ce6cb..0ff2e41 100644 --- a/framework/db/ar/ActiveMetaData.php +++ b/framework/db/ar/ActiveMetaData.php @@ -26,9 +26,9 @@ class ActiveMetaData */ 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. @@ -55,21 +55,18 @@ class ActiveMetaData public function __construct($modelClass) { $this->modelClass = $modelClass; - $tableName = $modelClass::tableName(); - $this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); + $this->model = new $modelClass; + $tableName = $this->model->tableName(); + $this->table = $this->model->getDbConnection()->getDriver()->getTableSchema($tableName); if ($this->table === null) { throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); } - $primaryKey = $modelClass::primaryKey(); - if ($primaryKey !== null) { + $primaryKey = $this->model->primaryKey(); + if ($primaryKey !== $this->table->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."); } - - foreach ($modelClass::relations() as $name => $config) { - $this->addRelation($name, $config); - } } /** diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index ce7291b..01c7037 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -10,10 +10,310 @@ namespace yii\db\ar; +use yii\db\dao\BaseQuery; use yii\base\VectorIterator; use yii\db\dao\Expression; 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 * 2. @@ -24,7 +324,7 @@ use yii\db\Exception; * @author Qiang Xue * @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. @@ -244,4 +544,38 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA { 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; + } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index f8370d0..5162c41 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/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. * 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. @@ -114,12 +104,12 @@ abstract class ActiveRecord extends Model * // find all active customers and order them by their age: * $customers = Customer::find() * ->where(array('status' => 1)) - * ->order('age') + * ->orderBy('age') * ->all(); * // or alternatively: * $customers = Customer::find(array( * 'where' => array('status' => 1), - * 'order' => 'age', + * 'orderBy' => 'age', * ))->all(); * ~~~ * @@ -141,14 +131,14 @@ abstract class ActiveRecord extends Model } } elseif ($q !== null) { // query by primary key - $primaryKey = static::getMetaData()->table->primaryKey; + $primaryKey = static::model()->primaryKey(); return $query->where(array($primaryKey[0] => $q))->one(); } 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 * query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect. * 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(); * ~~~ * - * @param array $q the query configuration. This should be an array of name-value pairs. - * It will be used to configure the [[ActiveQuery]] object for query purpose. + * @param array|string $q the query option. This can be one of the followings: * - * @return integer the counting result + * - 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 ActiveQuery the [[ActiveQuery]] instance */ public static function count($q = null) { @@ -195,11 +187,12 @@ abstract class ActiveRecord extends Model foreach ($q as $name => $value) { $query->$name = $value; } - } - if ($query->select === null) { + } elseif ($q !== null) { + $query->select = array($q); + } elseif ($query->select === null) { $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. - * This method is meant to be overridden in case when the table has no primary key defined - * (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. + * Returns the schema information of the DB table associated with this AR class. + * @return TableSchema the schema information of the DB table associated with this AR class. */ - public function primaryKey() + public function getTableSchema() { + return $this->getDbConnection()->getTableSchema($this->tableName()); } /** - * Declares the relations for this AR class. - * - * Child classes may override this method to specify their relations. - * - * The following code shows how to declare relations for a `Programmer` AR class: - * - * ~~~ - * 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: - * - * - * 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: - *
-	 * 'varName'=>array('relationType', 'className', 'foreign_key', ...additional options)
-	 * 
- * 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: - * - * - * The following options are available for certain relations when lazy loading: - * - * - * Below is an example declaring related objects for 'Post' active record class: - *
-	 * 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'),
-	 * );
-	 * 
- * - * @return array list of related object declarations. Defaults to empty array. + * Returns the primary keys for this AR class. + * The default implementation will return the primary keys as declared + * in the DB table that is associated with this AR class. + * If the DB table does not declare any primary key, you should override + * 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. */ - 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'. - *
-	 * return array(
-	 *	 'published'=>array(
-	 *		   'condition'=>'status=1',
-	 *	 ),
-	 *	 'recently'=>array(
-	 *		   'order'=>'create_time DESC',
-	 *		   'limit'=>5,
-	 *	 ),
-	 * );
-	 * 
- * If the above scopes are declared in a 'Post' model, we can perform the following - * queries: - *
-	 * $posts=Post::model()->published()->findAll();
-	 * $posts=Post::model()->published()->recently()->findAll();
-	 * $posts=Post::model()->published()->with('comments')->findAll();
-	 * 
- * 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. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -455,13 +316,13 @@ abstract class ActiveRecord extends Model if (isset($this->_attributes[$name])) { return $this->_attributes[$name]; } - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { 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)) { return $this->_related[$name]; } else { + // todo return $this->_related[$name] = $this->findByRelation($md->relations[$name]); } } @@ -476,10 +337,9 @@ abstract class ActiveRecord extends Model */ public function __set($name, $value) { - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { $this->_attributes[$name] = $value; - } elseif (isset($md->relations[$name])) { + } elseif (method_exists($this, $name)) { $this->_related[$name] = $value; } else { parent::__set($name, $value); @@ -498,8 +358,7 @@ abstract class ActiveRecord extends Model if (isset($this->_attributes[$name]) || isset($this->_related[$name])) { return true; } - $md = $this->getMetaData(); - if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { + if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) { return false; } else { return parent::__isset($name); @@ -514,10 +373,9 @@ abstract class ActiveRecord extends Model */ public function __unset($name) { - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { unset($this->_attributes[$name]); - } elseif (isset($md->relations[$name])) { + } elseif (method_exists($this, $name)) { unset($this->_related[$name]); } else { 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. * This method is internally used by [[ActiveQuery]] when populating relation data. * @param ActiveRelation $relation the relation object @@ -609,19 +450,7 @@ abstract class ActiveRecord extends Model */ public function attributes() { - return array_keys($this->getMetaData()->table->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(); + return array_keys($this->getTableSchema()->columns); } /** @@ -726,7 +555,7 @@ abstract class ActiveRecord extends Model $db = $this->getDbConnection(); $command = $query->insert($this->tableName(), $values)->createCommand($db); if ($command->execute()) { - $table = $this->getMetaData()->table; + $table = $this->getTableSchema(); if ($table->sequenceName !== null) { foreach ($table->primaryKey as $name) { if (!isset($this->_attributes[$name])) { @@ -973,12 +802,12 @@ abstract class ActiveRecord extends Model */ public function getPrimaryKey($asArray = false) { - $table = static::getMetaData()->table; - if (count($table->primaryKey) === 1 && !$asArray) { - return isset($this->_attributes[$table->primaryKey[0]]) ? $this->_attributes[$table->primaryKey[0]] : null; + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; } else { $values = array(); - foreach ($table->primaryKey as $name) { + foreach ($keys as $name) { $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; } return $values; @@ -998,12 +827,12 @@ abstract class ActiveRecord extends Model */ public function getOldPrimaryKey($asArray = false) { - $table = static::getMetaData()->table; - if (count($table->primaryKey) === 1 && !$asArray) { - return isset($this->_oldAttributes[$table->primaryKey[0]]) ? $this->_oldAttributes[$table->primaryKey[0]] : null; + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; } else { $values = array(); - foreach ($table->primaryKey as $name) { + foreach ($keys as $name) { $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; } return $values; @@ -1018,7 +847,7 @@ abstract class ActiveRecord extends Model public static function create($row) { $record = static::instantiate($row); - $columns = static::getMetaData()->table->columns; + $columns = static::model()->getTableSchema()->columns; foreach ($row as $name => $value) { if (isset($columns[$name])) { $record->_attributes[$name] = $value; @@ -1042,8 +871,7 @@ abstract class ActiveRecord extends Model */ public static function instantiate($row) { - $class = get_called_class(); - return new $class; + return new static; } /** diff --git a/framework/db/dao/Connection.php b/framework/db/dao/Connection.php index 85f29b4..6bb1576 100644 --- a/framework/db/dao/Connection.php +++ b/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. * @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 diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 0ad8466..2b6ac21 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -1,28 +1,29 @@ array( - 'link' => array('customer_id' => 'id'), - ), - ); + return $this->hasMany('Order', array('customer_id' => 'id')); } + /** + * @param ActiveQuery $query + * @return ActiveQuery + */ public function active($query) { - return $query->andWhere('@.`status` = ' . self::STATUS_ACTIVE); + return $query->andWhere('`status` = ' . self::STATUS_ACTIVE); } } \ No newline at end of file diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index b56c1c8..1b45f1e 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -4,14 +4,8 @@ namespace yiiunit\data\ar; class Item extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_item'; } - - public static function relations() - { - return array( - ); - } } \ No newline at end of file diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 09365f0..c9ed5b2 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -4,40 +4,30 @@ namespace yiiunit\data\ar; class Order extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_order'; } - public static function relations() + public function customer() { - return array( - 'customer:Customer' => array( - 'link' => array('id' => 'customer_id'), - ), - 'orderItems:OrderItem' => array( - 'link' => array('order_id' => 'id'), - ), - 'items:Item[]' => array( - 'via' => 'orderItems', - 'link' => array( - 'id' => 'item_id', - ), - 'order' => '@.id', - ), - 'books:Item[]' => array( - 'joinType' => 'INNER JOIN', - 'via' => array( - 'table' => 'tbl_order_item', - 'link' => array( - 'order_id' => 'id', - ), - ), - 'link' => array( - 'id' => 'item_id', - ), - 'on' => '@.category_id = 1', - ), - ); + return $this->hasOne('Customer', array('id' => 'customer_id')); + } + + public function orderItems() + { + return $this->hasMany('OrderItem', array('order_id' => 'id')); + } + + public function items() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems')->orderBy('id'); + } + + public function books() + { + return $this->manyMany('Item', array('id' => 'item_id'), 'tbl_order_item', array('item_id', 'id')) + ->where('category_id = 1'); } } \ No newline at end of file diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index c2ef796..0141f11 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -4,20 +4,18 @@ namespace yiiunit\data\ar; class OrderItem extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_order_item'; } - public static function relations() + public function order() { - return array( - 'order:Order' => array( - 'link' => array('order_id' => 'id'), - ), - 'item:Item' => array( - 'link' => array('item_id' => 'id'), - ), - ); + return $this->hasOne('Order', array('id' => 'order_id')); + } + + public function item() + { + return $this->hasOne('Item', array('id' => 'item_id')); } } \ No newline at end of file diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index a9263bc..9b169eb 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -16,87 +16,6 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase 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() { // find one @@ -105,6 +24,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $customer = $result->one(); $this->assertTrue($customer instanceof Customer); $this->assertEquals(1, $result->count); + $this->assertEquals(1, count($result)); // find all $result = Customer::find(); @@ -138,6 +58,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $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 $customer = Customer::find(2); @@ -164,186 +85,337 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals(2, Customer::count(array( 'where' => 'id=1 OR id=2', ))); + $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); - - // count - $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); - $query->one(); - $this->assertEquals(3, $query->count); - $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); - $this->assertEquals(3, $query->count); - } - - public function testQueryMethods() - { - $customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); - $this->assertTrue($customer instanceof Customer); - $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')->order('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()->order('id')->asArray()->all(); - $this->assertEquals('user2', $customers[1]['name']); - - // index - $customers = Customer::find()->order('id')->index('name')->all(); - $this->assertEquals(2, $customers['user2']['id']); - } - - public function testEagerLoading() - { - // has many - $customers = Customer::find()->with('orders')->order('@.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')->order('@.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')->order('@.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')->order('@.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')->order('@.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->order('@.id DESC'); - }))->order('@.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')->order('@.id')->index('id')->asArray()->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue(isset($customers[1], $customers[2], $customers[3])); - $this->assertTrue(is_array($customers[1])); - $this->assertEquals(1, count($customers[1]['orders'])); - $this->assertEquals(2, count($customers[2]['orders'])); - $this->assertEquals(0, count($customers[3]['orders'])); - $this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); - - // 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()->order('@.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->order('@.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']); - } +// 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() +// { +// // find one +// $result = Customer::find(); +// $this->assertTrue($result instanceof ActiveQuery); +// $customer = $result->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(1, $result->count); +// +// // find all +// $result = Customer::find(); +// $customers = $result->all(); +// $this->assertTrue(is_array($customers)); +// $this->assertEquals(3, count($customers)); +// $this->assertTrue($customers[0] instanceof Customer); +// $this->assertTrue($customers[1] 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); +// +// // find by a single primary key +// $customer = Customer::find(2); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// // find by attributes +// $customer = Customer::find()->where(array('name' => 'user2'))->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(2, $customer->id); +// +// // find by Query +// $query = array( +// 'where' => 'id=:id', +// 'params' => array(':id' => 2), +// ); +// $customer = Customer::find($query)->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// // find count +// $this->assertEquals(3, Customer::find()->count()); +// $this->assertEquals(3, Customer::count()); +// $this->assertEquals(2, Customer::count(array( +// '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); +// +// // count +// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); +// $query->one(); +// $this->assertEquals(3, $query->count); +// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); +// $this->assertEquals(3, $query->count); +// } +// +// public function testQueryMethods() +// { +// $customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); +// $this->assertTrue($customer instanceof Customer); +// $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']); +// } } \ No newline at end of file diff --git a/tests/unit/framework/db/dao/QueryTest.php b/tests/unit/framework/db/dao/QueryTest.php index 8779cd1..2a84617 100644 --- a/tests/unit/framework/db/dao/QueryTest.php +++ b/tests/unit/framework/db/dao/QueryTest.php @@ -56,14 +56,14 @@ class QueryTest extends \yiiunit\MysqlTestCase function testGroup() { $query = new Query; - $query->group('team'); - $this->assertEquals('team', $query->group); + $query->groupBy('team'); + $this->assertEquals('team', $query->groupBy); $query->addGroup('company'); - $this->assertEquals(array('team', 'company'), $query->group); + $this->assertEquals(array('team', 'company'), $query->groupBy); $query->addGroup('age'); - $this->assertEquals(array('team', 'company', 'age'), $query->group); + $this->assertEquals(array('team', 'company', 'age'), $query->groupBy); } function testHaving() @@ -85,14 +85,14 @@ class QueryTest extends \yiiunit\MysqlTestCase function testOrder() { $query = new Query; - $query->order('team'); - $this->assertEquals('team', $query->order); + $query->orderBy('team'); + $this->assertEquals('team', $query->orderBy); - $query->addOrder('company'); - $this->assertEquals(array('team', 'company'), $query->order); + $query->addOrderBy('company'); + $this->assertEquals(array('team', 'company'), $query->orderBy); - $query->addOrder('age'); - $this->assertEquals(array('team', 'company', 'age'), $query->order); + $query->addOrderBy('age'); + $this->assertEquals(array('team', 'company', 'age'), $query->orderBy); } function testLimitOffset()