diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php new file mode 100644 index 0000000..da4fd1a --- /dev/null +++ b/framework/db/ar/ActiveQuery.php @@ -0,0 +1,453 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\ar; + +use yii\base\VectorIterator; +use yii\db\dao\BaseQuery; +use yii\db\Exception; + +/** + * ActiveFinder.php is ... + * todo: add SQL monitor + * todo: better handling on join() support in QueryBuilder: use regexp to detect table name and quote it + * todo: do not support anonymous parameter binding + * todo: add ActiveFinderBuilder + * todo: quote join/on part of the relational query + * todo: modify QueryBuilder about join() methods + * todo: unify ActiveFinder and ActiveRelation in query building process + * todo: intelligent table aliasing (first table name, then relation name, finally t?) + * todo: allow using tokens in primary query fragments + * todo: findBySql + * todo: base limited + * todo: lazy loading + * todo: scope + * todo: test via option + * todo: count, sum, exists + * + * @property integer $count + * + * @author Qiang Xue + * @since 2.0 + */ +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 table alias to be used for query + */ + public $tableAlias; + /** + * @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 $indexBy; + /** + * @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 array list of query results + */ + public $records; + public $sql; + + /** + * @param string $modelClass the name of the ActiveRecord class. + */ + public function __construct($modelClass) + { + $this->modelClass = $modelClass; + } + + 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 indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + public function tableAlias($value) + { + $this->tableAlias = $value; + return $this; + } + + /** + * Executes query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all() + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return $this->records; + } + + /** + * Executes query and returns a single row of result. + * @return null|array|ActiveRecord the single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one() + { + if ($this->records === null) { + // todo: load only one record + $this->records = $this->findRecords(); + } + return isset($this->records[0]) ? $this->records[0] : null; + } + + public function value() + { + return 0; + } + + public function exists() + { + return $this->select(array('1'))->asArray(true)->one() !== null; + } + + /** + * 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 CCacheDependency $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 joinWith() + { + // todo: inner join with one or multiple relations as filters + } + + protected function findRecords() + { + if (!empty($this->with)) { + // todo: handle findBySql() and limit cases + $joinTree = $this->buildRelationalQuery(); + } + + if ($this->sql === null) { + $this->initFrom($this->query); + $command = $this->query->createCommand($this->getDbConnection()); + $this->sql = $command->getSql(); + } else { + $command = $this->getDbConnection()->createCommand($this->sql); + $command->bindValues($this->query->params); + } + + $rows = $command->queryAll(); + + if (isset($joinTree)) { + foreach ($rows as $row) { + $joinTree->populateData($row); + } + return array_values($joinTree->records); + } + + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + $records = array(); + foreach ($rows as $row) { + $records[$row[$this->indexBy]] = $row; + } + return $records; + } else { + $records = array(); + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $records[] = $class::populateData($row); + } + } else { + $attribute = $this->indexBy; + foreach ($rows as $row) { + $record = $class::populateData($row); + $records[$record->$attribute] = $record; + } + } + return $records; + } + } + + protected function initFrom($query) + { + if ($query->from === null) { + $modelClass = $this->modelClass; + $tableName = $modelClass::tableName(); + if ($this->tableAlias !== null) { + $tableName .= ' ' . $this->tableAlias; + } + $query->from = array($tableName); + } + } + + protected function buildRelationalQuery() + { + $joinTree = new JoinElement($this, null, null); + $this->buildJoinTree($joinTree, $this->with); + $this->buildTableAlias($joinTree); + $query = new Query; + foreach ($joinTree->children as $child) { + $child->buildQuery($query); + } + + $select = $joinTree->buildSelect($this->query->select); + if (!empty($query->select)) { + $this->query->select = array_merge($select, $query->select); + } else { + $this->query->select = $select; + } + if (!empty($query->where)) { + $this->query->andWhere('(' . implode(') AND (', $query->where) . ')'); + } + if (!empty($query->having)) { + $this->query->andHaving('(' . implode(') AND (', $query->having) . ')'); + } + if (!empty($query->join)) { + if ($this->query->join === null) { + $this->query->join = $query->join; + } else { + $this->query->join = array_merge($this->query->join, $query->join); + } + } + if (!empty($query->orderBy)) { + $this->query->addOrderBy($query->orderBy); + } + if (!empty($query->groupBy)) { + $this->query->addGroupBy($query->groupBy); + } + if (!empty($query->params)) { + $this->query->addParams($query->params); + } + + return $joinTree; + } + + /** + * @param JoinElement $parent + * @param array|string $with + * @param array $config + * @return null|JoinElement + * @throws \yii\db\Exception + */ + protected function buildJoinTree($parent, $with, $config = array()) + { + if (is_array($with)) { + foreach ($with as $name => $value) { + if (is_string($value)) { + $this->buildJoinTree($parent, $value); + } elseif (is_string($name) && is_array($value)) { + $this->buildJoinTree($parent, $name, $value); + } + } + return null; + } + + if (($pos = strrpos($with, '.')) !== false) { + $parent = $this->buildJoinTree($parent, substr($with, 0, $pos)); + $with = substr($with, $pos + 1); + } + + if (isset($parent->children[$with])) { + $child = $parent->children[$with]; + $child->joinOnly = false; + } else { + $modelClass = $parent->relation->modelClass; + $relations = $modelClass::getMetaData()->relations; + if (!isset($relations[$with])) { + throw new Exception("$modelClass has no relation named '$with'."); + } + $relation = clone $relations[$with]; + if ($relation->via !== null && isset($relations[$relation->via])) { + $relation->via = null; + $parent2 = $this->buildJoinTree($parent, $relation->via); + if ($parent2->joinOnly === null) { + $parent2->joinOnly = true; + } + $child = new JoinElement($relation, $parent2, $parent); + } else { + $child = new JoinElement($relation, $parent, $parent); + } + } + + foreach ($config as $name => $value) { + $child->relation->$name = $value; + } + + return $child; + } + + protected function buildTableAlias($element, &$count = 0) + { + if ($element->relation->tableAlias === null) { + $element->relation->tableAlias = 't' . ($count++); + } + foreach ($element->children as $child) { + $this->buildTableAlias($child, $count); + } + } +} diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index fbde605..76a5883 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -38,6 +38,10 @@ abstract class ActiveRecord extends Model * @var array old attribute values indexed by attribute names. */ private $_oldAttributes; + /** + * @var array related records indexed by relation names. + */ + private $_related; /** * Returns the metadata for this AR class. @@ -61,104 +65,167 @@ abstract class ActiveRecord extends Model } /** - * Creates an [[ActiveFinder]] instance for query purpose. + * Creates an [[ActiveQuery]] instance for query purpose. * - * Because [[ActiveFinder]] implements a set of query building methods, - * additional query conditions can be specified by calling these methods. + * Because [[ActiveQuery]] implements a set of query building methods, + * additional query conditions can be specified by calling the methods of [[ActiveQuery]]. * * Below are some usage examples: * * ~~~ * // find all customers * $customers = Customer::find()->all(); - * // find a single customer whose ID is 10 + * // find a single customer whose primary key value is 10 * $customer = Customer::find(10)->one(); * // find all active customers and order them by their age: - * $customers = Customer::find(array('status' => 1))->orderBy('age')->all(); + * $customers = Customer::find() + * ->where(array('status' => 1)) + * ->orderBy('age') + * ->all(); + * // or alternatively: + * $customers = Customer::find(array( + * 'where' => array('status' => 1), + * 'orderBy' => 'age', + * ))->all(); * ~~~ * * @param mixed $q the query parameter. This can be one of the followings: * - * - a scalar value (integer, string): query by a single primary key value. - * - an array of name-value pairs: query by a set of column values. - * - a [[Query]] object: query by a full query object. + * - a scalar value (integer or string): query by a single primary key value. + * - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object. * - * @return ActiveFinder the [[ActiveFinder]] instance for query purpose. - * @throws Exception if the query parameter is invalid. + * @return ActiveQuery the [[ActiveQuery]] instance for query purpose. */ public static function find($q = null) { - $finder = static::createActiveFinder(); - if ($q instanceof Query) { - $finder->query = $q; - } elseif (is_array($q)) { - // query by a set of column values - $finder->where($q); + $query = static::createActiveQuery(); + if (is_array($q)) { + foreach ($q as $name => $value) { + $query->$name = $value; + } } elseif ($q !== null) { // query by primary key $primaryKey = static::getMetaData()->table->primaryKey; - if (count($primaryKey) === 1) { - $finder->where(array($primaryKey[0] => $q)); - } else { - throw new Exception('Multiple values are required to query by composite primary keys.'); - } + $query->where(array($primaryKey[0] => $q)); } - return $finder; + return $query; } /** - * Creates an [[ActiveFinder]] instance and query by a given SQL statement. + * Creates an [[ActiveQuery]] instance and query by a given SQL statement. * Note that because the SQL statement is already specified, calling further - * query methods (such as `where()`, `orderBy()`) on [[ActiveFinder]] will have no effect. + * query methods (such as `where()`, `orderBy()`) on [[ActiveQuery]] will have no effect. + * Methods such as `with()`, `asArray()` can still be called though. * @param string $sql the SQL statement to be executed * @param array $params parameters to be bound to the SQL statement during execution. - * @return ActiveFinder the [[ActiveFinder]] instance + * @return ActiveQuery the [[ActiveQuery]] instance */ public static function findBySql($sql, $params = array()) { - $finder = static::createActiveFinder(); - $finder->sql = $sql; - return $finder->params($params); + $query = static::createActiveQuery(); + $query->sql = $sql; + return $query->params($params); + } + + /** + * Performs a COUNT query for this AR class. + * + * Below are some usage examples: + * + * ~~~ + * // count the total number of customers + * echo Customer::count(); + * // count the number of customers whose primary key value is 10. + * echo Customer::count(10); + * // count the number of active customers: + * echo Customer::count(array( + * 'where' => array('status' => 1), + * )); + * ~~~ + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value. + * - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object for query purpose. + * + * @return integer the counting result + */ + public static function count($q = null) + { + $query = static::createActiveQuery(); + if (is_array($q)) { + foreach ($q as $name => $value) { + $query->$name = $value; + } + } elseif ($q !== null) { + // query by primary key + $primaryKey = static::getMetaData()->table->primaryKey; + $query->where(array($primaryKey[0] => $q)); + } + if ($query->select === null) { + $query->select = 'COUNT(*)'; + } + return $query->value(); } + /** + * Updates the whole table using the provided attribute values and conditions. + * @param array $attributes attribute values to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return integer the number of rows updated + */ public static function updateAll($attributes, $condition = '', $params = array()) { - $class = get_called_class(); $query = new Query; - $query->update($class::tableName(), $attributes, $condition, $params); - return $query->createCommand($class::getDbConnection())->execute(); + $query->update(static::tableName(), $attributes, $condition, $params); + return $query->createCommand(static::getDbConnection())->execute(); } - public static function updateCounters($counters, $condition = '', $params = array()) + /** + * Updates the whole table using the provided counter values and conditions. + * @param array $counters the counters to be updated (attribute name => increment value). + * @param string|array $condition the conditions that will be put in the WHERE part. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = '', $params = array()) { - $class = get_called_class(); - $db = $class::getDbConnection(); + $db = static::getDbConnection(); foreach ($counters as $name => $value) { $value = (int)$value; $quotedName = $db->quoteColumnName($name, true); $counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value"); } $query = new Query; - $query->update($class::tableName(), $counters, $condition, $params); - return $query->createCommand($class::getDbConnection())->execute(); + $query->update(static::tableName(), $counters, $condition, $params); + return $query->createCommand($db)->execute(); } + /** + * Deletes rows in the table using the provided conditions. + * @param string|array $condition the conditions that will be put in the WHERE part. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return integer the number of rows updated + */ public static function deleteAll($condition = '', $params = array()) { - $class = get_called_class(); $query = new Query; - $query->delete($class::tableName(), $condition, $params); - return $query->createCommand($class::getDbConnection())->execute(); + $query->delete(static::tableName(), $condition, $params); + return $query->createCommand(static::getDbConnection())->execute(); } /** - * Creates a [[ActiveFinder]] instance. - * This method is mainly called by [[find()]] and [[findBySql()]]. - * @return ActiveFinder the newly created [[ActiveFinder]] instance. + * Creates a [[ActiveQuery]] instance. + * This method is called by [[find()]] and [[findBySql()]] to start a SELECT query. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. */ - public static function createActiveFinder() + public static function createActiveQuery() { - return new ActiveFinder(get_called_class()); + return new ActiveQuery(get_called_class()); } /** @@ -175,8 +242,8 @@ 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 is not defined with a primary key - * (for some legacy database). If the table is already defined with a primary key, + * 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. @@ -193,7 +260,7 @@ abstract class ActiveRecord extends Model * * Child classes may override this method to specify their relations. * - * The following shows how to declare relations for a `Programmer` AR class: + * The following code shows how to declare relations for a `Programmer` AR class: * * ~~~ * return array( @@ -350,14 +417,17 @@ abstract class ActiveRecord extends Model */ public function __get($name) { - if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { + if (isset($this->_attributes[$name])) { return $this->_attributes[$name]; - } else { - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { - return null; - } elseif (isset($md->relations[$name])) { - return $this->_attributes[$name] = $this->loadRelatedRecord($md->relations[$name]); + } + $md = $this->getMetaData(); + if (isset($md->table->columns[$name])) { + return null; + } elseif (isset($md->relations[$name])) { + if (array_key_exists($name, $this->_related)) { + return $this->_related[$name]; + } else { + return $this->_related[$name] = $this->loadRelatedRecord($md->relations[$name]); } } return parent::__get($name); @@ -372,8 +442,10 @@ abstract class ActiveRecord extends Model public function __set($name, $value) { $md = $this->getMetaData(); - if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { + if (isset($md->table->columns[$name])) { $this->_attributes[$name] = $value; + } elseif (isset($md->relations[$name])) { + $this->_related[$name] = $value; } else { parent::__set($name, $value); } @@ -388,9 +460,11 @@ abstract class ActiveRecord extends Model */ public function __isset($name) { - if (isset($this->_attributes[$name])) { + if (isset($this->_attributes[$name]) || isset($this->_related[$name])) { return true; - } elseif (isset($this->getMetaData()->table->columns[$name]) || isset($this->getMetaData()->relations[$name])) { + } + $md = $this->getMetaData(); + if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { return false; } else { return parent::__isset($name); @@ -406,8 +480,10 @@ abstract class ActiveRecord extends Model public function __unset($name) { $md = $this->getMetaData(); - if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { + if (isset($md->table->columns[$name])) { unset($this->_attributes[$name]); + } elseif (isset($md->relations[$name])) { + unset($this->_related[$name]); } else { parent::__unset($name); } @@ -432,20 +508,20 @@ abstract class ActiveRecord extends Model /** * Initializes the internal storage for the relation. - * This method is internally used by [[ActiveFinder]] when populating relation data. + * This method is internally used by [[ActiveQuery]] when populating relation data. * @param ActiveRelation $relation the relation object */ public function initRelation($relation) { - $this->_attributes[$relation->name] = $relation->hasMany ? array() : null; + $this->_related[$relation->name] = $relation->hasMany ? array() : null; } public function addRelatedRecord($relation, $record) { if ($relation->hasMany) { - $this->_attributes[$relation->name][] = $record; + $this->_related[$relation->name][] = $record; } else { - $this->_attributes[$relation->name] = $record; + $this->_related[$relation->name] = $record; } } @@ -472,7 +548,7 @@ abstract class ActiveRecord extends Model } $relation = $md->relations[$relation]; } - $finder = $this->createActiveFinder(); + $finder = $this->createActiveQuery(); } /** @@ -541,7 +617,7 @@ abstract class ActiveRecord extends Model } $names = array_flip($names); $attributes = array(); - if (empty($this->_oldAttributes)) { + if ($this->_oldAttributes === null) { foreach ($this->_attributes as $name => $value) { if (isset($names[$name])) { $attributes[$name] = $value; @@ -584,6 +660,124 @@ abstract class ActiveRecord extends Model { if (!$runValidation || $this->validate($attributes)) { return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes); + } + return false; + } + + /** + * Inserts a row into the table based on this active record attributes. + * If the table's primary key is auto-incremental and is null before insertion, + * it will be populated with the actual value after insertion. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false, + * and its {@link scenario} property will be set to be 'update'. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws Exception if the record is not new + */ + public function insert($attributes = null) + { + if ($this->beforeInsert()) { + $query = new Query; + $values = $this->getChangedAttributes($attributes); + $db = $this->getDbConnection(); + $command = $query->insert($this->tableName(), $values)->createCommand($db); + if ($command->execute()) { + $table = $this->getMetaData()->table; + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $name) { + if (!isset($this->_attributes[$name])) { + $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); + break; + } + } + } + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $value; + } + $this->afterInsert(); + return true; + } + } + return false; + } + + /** + * Updates the row represented by this active record. + * All loaded attributes will be saved to the database. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the update is successful + * @throws Exception if the record is new + */ + public function update($attributes = null) + { + if ($this->getIsNewRecord()) { + throw new Exception('The active record cannot be updated because it is new.'); + } + if ($this->beforeUpdate()) { + $values = $this->getChangedAttributes($attributes); + if ($values !== array()) { + $this->updateAll($values, $this->getOldPrimaryKey(true)); + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + $this->afterUpdate(); + } + return true; + } else { + return false; + } + } + + /** + * Saves one or several counter columns for the current AR object. + * Note that this method differs from [[updateAllCounters()]] in that it only + * saves counters for the current AR object. + * + * An example usage is as follows: + * + * ~~~ + * $post = Post::find($id)->one(); + * $post->updateCounters(array('view_count' => 1)); + * ~~~ + * + * Use negative values if you want to decrease the counters. + * @param array $counters the counters to be updated (attribute name => increment value) + * @return boolean whether the saving is successful + * @throws Exception if the record is new or any database error + * @see updateAllCounters() + */ + public function updateCounters($counters) + { + if ($this->getIsNewRecord()) { + throw new Exception('The active record cannot be updated because it is new.'); + } + $this->updateAllCounters($counters, $this->getOldPrimaryKey(true)); + foreach ($counters as $name => $value) { + $this->_attributes[$name] += $value; + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + return true; + } + + /** + * Deletes the row corresponding to this active record. + * @return boolean whether the deletion is successful. + * @throws Exception if the record is new or any database error + */ + public function delete() + { + if ($this->getIsNewRecord()) { + throw new Exception('The active record cannot be deleted because it is new.'); + } + if ($this->beforeDelete()) { + $result = $this->deleteAll($this->getPrimaryKey(true)) > 0; + $this->_oldAttributes = null; + $this->afterDelete(); + return $result; } else { return false; } @@ -598,7 +792,7 @@ abstract class ActiveRecord extends Model */ public function getIsNewRecord() { - return empty($this->_oldAttributes); + return $this->_oldAttributes === null; } /** @@ -608,41 +802,51 @@ abstract class ActiveRecord extends Model */ public function setIsNewRecord($value) { - if ($value) { - $this->_oldAttributes = null; - } else { - $this->_oldAttributes = array(); - foreach ($this->attributeNames() as $name) { - if (isset($this->_attributes[$name])) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - } - } + $this->_oldAttributes = $value ? null : $this->_attributes; } /** * This event is raised before the record is saved. - * By setting {@link ModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. - * @param ModelEvent $event the event parameter + * By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[save()]] will be stopped. + * @param \yii\base\ModelEvent $event the event parameter */ - public function onBeforeSave($event) + public function onBeforeInsert($event) { - $this->raiseEvent('onBeforeSave', $event); + $this->raiseEvent('onBeforeInsert', $event); } /** * This event is raised after the record is saved. - * @param Event $event the event parameter + * @param \yii\base\Event $event the event parameter */ - public function onAfterSave($event) + public function onAfterInsert($event) { - $this->raiseEvent('onAfterSave', $event); + $this->raiseEvent('onAfterInsert', $event); + } + + /** + * This event is raised before the record is saved. + * By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[save()]] will be stopped. + * @param \yii\base\ModelEvent $event the event parameter + */ + public function onBeforeUpdate($event) + { + $this->raiseEvent('onBeforeUpdate', $event); + } + + /** + * This event is raised after the record is saved. + * @param \yii\base\Event $event the event parameter + */ + public function onAfterUpdate($event) + { + $this->raiseEvent('onAfterUpdate', $event); } /** * This event is raised before the record is deleted. - * By setting {@link ModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. - * @param ModelEvent $event the event parameter + * By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[delete()]] process will be stopped. + * @param \yii\base\ModelEvent $event the event parameter */ public function onBeforeDelete($event) { @@ -651,7 +855,7 @@ abstract class ActiveRecord extends Model /** * This event is raised after the record is deleted. - * @param Event $event the event parameter + * @param \yii\base\Event $event the event parameter */ public function onAfterDelete($event) { @@ -667,10 +871,37 @@ abstract class ActiveRecord extends Model * Make sure you call the parent implementation so that the event is raised properly. * @return boolean whether the saving should be executed. Defaults to true. */ - public function beforeSave() + public function beforeInsert() + { + $event = new ModelEvent($this); + $this->onBeforeInsert($event); + return $event->isValid; + } + + /** + * This method is invoked after saving a record successfully. + * The default implementation raises the {@link onAfterSave} event. + * You may override this method to do postprocessing after record saving. + * Make sure you call the parent implementation so that the event is raised properly. + */ + public function afterInsert() + { + $this->onAfterInsert(new Event($this)); + } + + /** + * This method is invoked before saving a record (after validation, if any). + * The default implementation raises the {@link onBeforeSave} event. + * You may override this method to do any preparation work for record saving. + * Use {@link isNewRecord} to determine whether the saving is + * for inserting or updating record. + * Make sure you call the parent implementation so that the event is raised properly. + * @return boolean whether the saving should be executed. Defaults to true. + */ + public function beforeUpdate() { $event = new ModelEvent($this); - $this->onBeforeSave($event); + $this->onBeforeUpdate($event); return $event->isValid; } @@ -680,9 +911,9 @@ abstract class ActiveRecord extends Model * You may override this method to do postprocessing after record saving. * Make sure you call the parent implementation so that the event is raised properly. */ - public function afterSave() + public function afterUpdate() { - $this->onAfterSave(new Event($this)); + $this->onAfterUpdate(new Event($this)); } /** @@ -711,182 +942,30 @@ abstract class ActiveRecord extends Model } /** - * Inserts a row into the table based on this active record attributes. - * If the table's primary key is auto-incremental and is null before insertion, - * it will be populated with the actual value after insertion. - * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. - * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false, - * and its {@link scenario} property will be set to be 'update'. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - * @throws Exception if the record is not new - */ - public function insert($attributes = null) - { - if ($this->beforeSave()) { - $db = $this->getDbConnection(); - $query = new Query; - $values = $this->getChangedAttributes($attributes); - $command = $query->insert($this->tableName(), $values)->createCommand($db); - if ($command->execute()) { - $table = $this->getMetaData()->table; - if ($table->sequenceName !== null) { - foreach ($table->primaryKey as $name) { - if ($this->$name === null) { - $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); - break; - } - } - } - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - $this->afterSave(); - $this->setIsNewRecord(false); - return true; - } - } - return false; - } - - /** - * Updates the row represented by this active record. - * All loaded attributes will be saved to the database. - * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the update is successful - * @throws Exception if the record is new - */ - public function update($attributes = null) - { - if ($this->beforeSave()) { - $values = $this->getChangedAttributes($attributes); - if ($values !== array()) { - $this->updateAll($values, $this->getOldPrimaryKey(true)); - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - } - $this->afterSave(); - $this->setIsNewRecord(false); - return true; - } else { - return false; - } - } - - /** - * Saves a selected list of attributes. - * Unlike {@link save}, this method only saves the specified attributes - * of an existing row and does NOT call either {@link beforeSave} or {@link afterSave}. - * Also note that this method does not validate attributes. - * So do not use this method with untrusted data (such as user posted data). - * You may consider the following alternative if you want to do so: - * - * ~~~ - * $user = User::find($id)->one; - * $user->attributes = $_POST['User']; - * $user->save(); - * ~~~ - * - * @param array $attributes attributes to be updated. Each element represents an attribute name - * or an attribute value indexed by its name. If the latter, the record's - * attribute will be changed accordingly before saving. - * @return boolean whether the update is successful - * @throws Exception if the record is new or any database error - */ - public function saveAttributes($attributes) - { - if (!$this->getIsNewRecord()) { - $values = array(); - foreach ($attributes as $name => $value) { - if (is_integer($name)) { - $values[$value] = $this->$value; - } else { - $values[$name] = $this->$name = $value; - } - } - $this->updateAll($values, $this->getOldPrimaryKey(true)); - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $value; - } - return true; - } else { - throw new Exception('The active record cannot be updated because it is new.'); - } - } - - /** - * Saves one or several counter columns for the current AR object. - * Note that this method differs from {@link updateCounters} in that it only - * saves the current AR object. - * An example usage is as follows: - *
-	 * $postRecord=Post::model()->findByPk($postID);
-	 * $postRecord->saveCounters(array('view_count'=>1));
-	 * 
- * Use negative values if you want to decrease the counters. - * @param array $counters the counters to be updated (column name=>increment value) - * @return boolean whether the saving is successful - * @see updateCounters - */ - public function saveCounters($counters) - { - if (!$this->getIsNewRecord()) { - $this->updateCounters($counters, $this->getOldPrimaryKey(true)); - foreach ($counters as $name => $value) { - $this->$name += $value; - $this->_oldAttributes[$name] = $this->$name; - } - return true; - } else { - throw new Exception('The active record cannot be updated because it is new.'); - } - } - - /** - * Deletes the row corresponding to this active record. - * @return boolean whether the deletion is successful. - * @throws Exception if the record is new - */ - public function delete() - { - if (!$this->getIsNewRecord()) { - if ($this->beforeDelete()) { - $result = $this->deleteAll($this->getPrimaryKey(true)) > 0; - $this->_oldAttributes = null; - $this->afterDelete(); - return $result; - } else { - return false; - } - } else { - throw new Exception('The active record cannot be deleted because it is new.'); - } - } - - /** * Repopulates this active record with the latest data. * @param array $attributes * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record. */ public function refresh($attributes = null) { - if (!$this->getIsNewRecord() && ($record = $this->find($this->getPrimaryKey(true))) !== null) { - if ($attributes === null) { - $attributes = $this->attributeNames(); - } - $this->_attributes = array(); - foreach ($attributes as $name) { + if ($this->getIsNewRecord()) { + return false; + } + $record = $this->find()->where($this->getPrimaryKey(true))->one(); + if ($record === null) { + return false; + } + if ($attributes === null) { + foreach ($this->attributeNames() as $name) { $this->_attributes[$name] = $record->_attributes[$name]; } $this->_oldAttributes = $this->_attributes; - return true; } else { - return false; + foreach ($attributes as $name) { + $this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name]; + } } + return true; } /** @@ -911,11 +990,11 @@ abstract class ActiveRecord extends Model { $table = static::getMetaData()->table; if (count($table->primaryKey) === 1 && !$asArray) { - return $this->{$table->primaryKey[0]}; + return isset($this->_attributes[$table->primaryKey[0]]) ? $this->_attributes[$table->primaryKey[0]] : null; } else { $values = array(); foreach ($table->primaryKey as $name) { - $values[$name] = $this->$name; + $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; } return $values; } @@ -928,7 +1007,8 @@ abstract class ActiveRecord extends Model * The value remains unchanged even if the primary key attribute is manually assigned with a different value. * @param boolean $asArray whether to return the primary key value as an array. If true, * the return value will be an array with column name as key and column value as value. - * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If this is false (default), a scalar value will be returned for non-composite primary key. + * @return string|array the old primary key value. An array (column name=>column value) is returned if the primary key is composite. * If primary key is not defined, null will be returned. */ public function getOldPrimaryKey($asArray = false) @@ -948,19 +1028,19 @@ abstract class ActiveRecord extends Model /** * Creates an active record with the given attributes. * This method is internally used by the find methods. - * * @param array $row attribute values (column name=>column value) - * * @return ActiveRecord the newly created active record. The class of the object is the same as the model class. * Null is returned if the input data is false. */ - public static function populateData($row) + public static function createRecord($row) { $record = static::instantiate($row); $columns = static::getMetaData()->table->columns; foreach ($row as $name => $value) { if (isset($columns[$name])) { $record->_attributes[$name] = $value; + } elseif ($record->canSetProperty($name)) { + $record->$name = $value; } } $record->_oldAttributes = $record->_attributes; @@ -969,7 +1049,7 @@ abstract class ActiveRecord extends Model /** * Creates an active record instance. - * This method is called by {@link populateData}. + * This method is called by [[createRecord()]]. * You may override this method if the instance being created * depends the attributes that are to be populated to the record. * For example, by creating a record based on the value of a column, diff --git a/framework/db/ar/ActiveRecordBehavior.php b/framework/db/ar/ActiveRecordBehavior.php index 6971cde..9556701 100644 --- a/framework/db/ar/ActiveRecordBehavior.php +++ b/framework/db/ar/ActiveRecordBehavior.php @@ -27,8 +27,10 @@ class CActiveRecordBehavior extends CModelBehavior public function events() { return array_merge(parent::events(), array( - 'onBeforeSave' => 'beforeSave', - 'onAfterSave' => 'afterSave', + 'onBeforeInsert' => 'beforeInsert', + 'onAfterInsert' => 'afterInsert', + 'onBeforeUpdate' => 'beforeUpdate', + 'onAfterUpdate' => 'afterUpdate', 'onBeforeDelete' => 'beforeDelete', 'onAfterDelete' => 'afterDelete', 'onBeforeFind' => 'beforeFind', @@ -42,7 +44,7 @@ class CActiveRecordBehavior extends CModelBehavior * You may set {@link CModelEvent::isValid} to be false to quit the saving process. * @param CModelEvent $event event parameter */ - public function beforeSave($event) + public function beforeInsert($event) { } @@ -51,7 +53,26 @@ class CActiveRecordBehavior extends CModelBehavior * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. * @param CModelEvent $event event parameter */ - public function afterSave($event) + public function afterInsert($event) + { + } + + /** + * Responds to {@link CActiveRecord::onBeforeSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the saving process. + * @param CModelEvent $event event parameter + */ + public function beforeUpdate($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CModelEvent $event event parameter + */ + public function afterUpdate($event) { } diff --git a/framework/db/ar/ActiveRelation.php b/framework/db/ar/ActiveRelation.php index 3d1a06d..a6d63fb 100644 --- a/framework/db/ar/ActiveRelation.php +++ b/framework/db/ar/ActiveRelation.php @@ -1,67 +1,64 @@ + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ namespace yii\db\ar; -class ActiveRelation extends \yii\base\Object +use yii\db\dao\BaseQuery; + +/** + * ActiveRelation represents the specification of a relation declared in [[ActiveRecord::relations()]]. + * + * @author Qiang Xue + * @since 2.0 + */ +class ActiveRelation extends BaseQuery { + /** + * @var string the name of this relation + */ public $name; - public $modelClass; - public $hasMany; - - public $joinType; - public $tableAlias; - public $on; - public $via; - public $with; - public $scopes; - /** - * @var string|array the columns being selected. This refers to the SELECT clause in a SQL - * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). - * If not set, if means all columns. - * @see select() + * @var string the name of the model class that this relation represents */ - public $select; + public $modelClass; /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() + * @var boolean whether this relation is a one-many relation */ - public $where; + public $hasMany; /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + * @var string the join type (e.g. INNER JOIN, LEFT JOIN). Defaults to 'LEFT JOIN'. */ - public $limit; + public $joinType = 'LEFT JOIN'; /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. + * @var string the table alias used for the corresponding table during query */ - public $offset; + public $tableAlias; /** - * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. - * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). + * @var string the name of the column that the result should be indexed by. + * This is only useful when [[hasMany]] is true. */ - public $orderBy; + public $indexBy; /** - * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. - * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). + * @var string the ON clause of the join query */ - public $groupBy; + public $on; /** - * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. - * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. - * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). - * @see join() + * @var string */ - public $join; + public $via; /** - * @var string|array the condition to be applied in the GROUP BY clause. - * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. + * @var array the relations that should be queried together (eager loading) */ - public $having; + public $with; /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. + * @var array the scopes that should be applied during query */ - public $params; + public $scopes; } diff --git a/framework/db/dao/BaseQuery.php b/framework/db/dao/BaseQuery.php new file mode 100644 index 0000000..cf7daad --- /dev/null +++ b/framework/db/dao/BaseQuery.php @@ -0,0 +1,635 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\dao; + +/** + * BaseQuery is the base class that represents a SQL SELECT statement in a DBMS-independent way. + * + * @author Qiang Xue + * @since 2.0 + */ +class BaseQuery extends \yii\base\Object +{ + /** + * @var string|array the columns being selected. This refers to the SELECT clause in a SQL + * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). + * If not set, if means all columns. + * @see select() + */ + public $select; + /** + * @var string additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + */ + public $selectOption; + /** + * @var boolean whether to select distinct rows of data only. If this is set true, + * the SELECT clause would be changed to SELECT DISTINCT. + */ + public $distinct; + /** + * @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. + * It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). + * @see from() + */ + public $from; + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. + * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). + */ + public $orderBy; + /** + * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. + * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). + */ + public $groupBy; + /** + * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. + * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. + * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). + * @see join() + */ + public $join; + /** + * @var string|array the condition to be applied in the GROUP BY clause. + * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. + */ + public $having; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + */ + public $params; + /** + * @var string|BaseQuery[] the UNION clause(s) in a SQL statement. This can be either a string + * representing a single UNION clause or an array representing multiple UNION clauses. + * Each union clause can be a string or a `BaseQuery` object which refers to the SQL statement. + */ + public $union; + + /** + * Sets the SELECT part of the query. + * @param string|array $columns the columns to be selected. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + * @return BaseQuery the query object itself + */ + public function select($columns, $option = null) + { + $this->select = $columns; + $this->selectOption = $option; + return $this; + } + + /** + * Sets the value indicating whether to SELECT DISTINCT or not. + * @param bool $value whether to SELECT DISTINCT or not. + * @return BaseQuery the query object itself + */ + public function distinct($value = true) + { + $this->distinct = $value; + return $this; + } + + /** + * Sets the FROM part of the query. + * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'tbl_user'`) + * or an array (e.g. `array('tbl_user', 'tbl_profile')`) specifying one or several table names. + * Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`). + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return BaseQuery the query object itself + */ + public function from($tables) + { + $this->from = $tables; + return $this; + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $condition parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be in one of the following two formats: + * + * - hash format: `array('column1' => value1, 'column2' => value2, ...)` + * - operator format: `array(operator, operand1, operand2, ...)` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `array('type'=>1, 'status'=>2)` generates `(type=1) AND (status=2)`. + * - `array('id'=>array(1,2,3), 'status'=>2)` generates `(id IN (1,2,3)) AND (status=2)`. + * - `array('status'=>null) generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `array('and', 'id=1', 'id=2')` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `array('and', 'type=1', array('or', 'id=1', 'id=2'))` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `array('between', 'id', 1, 10)` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `array('in', 'id', array(1,2,3))` will generate `id IN (1,2,3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `array('like', 'name', '%tester%')` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `array('like', 'name', array('%test%', '%sample%'))` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param string|array $condition the conditions that should be put in the WHERE part. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition, $params = array()) + { + $this->where = $condition; + $this->addParams($params); + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition, $params = array()) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('and', $this->where, $condition); + } + $this->addParams($params); + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition, $params = array()) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('or', $this->where, $condition); + } + $this->addParams($params); + return $this; + } + + /** + * Appends a JOIN part to the query. + * The first parameter specifies what type of join it is. + * @param string $type the type of join, such as INNER JOIN, LEFT JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + */ + public function join($type, $table, $on = '', $params = array()) + { + $this->join[] = array($type, $table, $on); + return $this->addParams($params); + } + + /** + * Appends an INNER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + */ + public function innerJoin($table, $on = '', $params = array()) + { + $this->join[] = array('INNER JOIN', $table, $on); + return $this->addParams($params); + } + + /** + * Appends a LEFT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query + * @return BaseQuery the query object itself + */ + public function leftJoin($table, $on = '', $params = array()) + { + $this->join[] = array('LEFT JOIN', $table, $on); + return $this->addParams($params); + } + + /** + * Appends a RIGHT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query + * @return BaseQuery the query object itself + */ + public function rightJoin($table, $on = '', $params = array()) + { + $this->join[] = array('RIGHT JOIN', $table, $on); + return $this->addParams($params); + } + + /** + * Sets the GROUP BY part of the query. + * @param string|array $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return BaseQuery the query object itself + * @see addGroupBy() + */ + public function groupBy($columns) + { + $this->groupBy = $columns; + return $this; + } + + /** + * Adds additional group-by columns to the existing ones. + * @param string|array $columns additional columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return BaseQuery the query object itself + * @see groupBy() + */ + public function addGroupBy($columns) + { + if (empty($this->groupBy)) { + $this->groupBy = $columns; + } else { + if (!is_array($this->groupBy)) { + $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); + } + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->groupBy = array_merge($this->groupBy, $columns); + } + return $this; + } + + /** + * Sets the HAVING part of the query. + * @param string|array $condition the conditions to be put after HAVING. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see andHaving() + * @see orHaving() + */ + public function having($condition, $params = array()) + { + $this->having = $condition; + $this->addParams($params); + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see having() + * @see orHaving() + */ + public function andHaving($condition, $params = array()) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = array('and', $this->having, $condition); + } + $this->addParams($params); + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return BaseQuery the query object itself + * @see having() + * @see andHaving() + */ + public function orHaving($condition, $params = array()) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = array('or', $this->having, $condition); + } + $this->addParams($params); + return $this; + } + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return BaseQuery the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $columns; + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return BaseQuery the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + if (empty($this->orderBy)) { + $this->orderBy = $columns; + } else { + if (!is_array($this->orderBy)) { + $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); + } + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit + * @return BaseQuery the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset + * @return BaseQuery the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } + + /** + * Appends a SQL statement using UNION operator. + * @param string|BaseQuery $sql the SQL statement to be appended using UNION + * @return BaseQuery the query object itself + */ + public function union($sql) + { + $this->union[] = $sql; + return $this; + } + + /** + * Sets the parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + * @return BaseQuery the query object itself + * @see addParams() + */ + public function params($params) + { + $this->params = $params; + return $this; + } + + /** + * Adds additional parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + * @return BaseQuery the query object itself + * @see params() + */ + public function addParams($params) + { + if ($params !== array()) { + if ($this->params === null) { + $this->params = $params; + } else { + foreach ($params as $name => $value) { + if (is_integer($name)) { + $this->params[] = $value; + } else { + $this->params[$name] = $value; + } + } + } + } + return $this; + } + + /** + * Merges this query with another one. + * + * The merging is done according to the following rules: + * + * - [[select]]: the union of both queries' [[select]] property values. + * - [[selectOption]], [[distinct]], [[from]], [[limit]], [[offset]]: the new query + * takes precedence over this query. + * - [[where]], [[having]]: the new query's corresponding property value + * will be 'AND' together with the existing one. + * - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's + * corresponding property value will be appended to the existing one. + * + * In general, the merging makes the resulting query more restrictive and specific. + * @param BaseQuery $query the new query to be merged with this query. + * @return BaseQuery the query object itself + */ + public function mergeWith($query) + { + if ($this->select !== $query->select) { + if (empty($this->select)) { + $this->select = $query->select; + } elseif (!empty($query->select)) { + $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; + $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; + $this->select = array_merge($select1, array_diff($select2, $select1)); + } + } + + if ($query->selectOption !== null) { + $this->selectOption = $query->selectOption; + } + + if ($query->distinct !== null) { + $this->distinct = $query->distinct; + } + + if ($query->from !== null) { + $this->from = $query->from; + } + + if ($query->limit !== null) { + $this->limit = $query->limit; + } + + if ($query->offset !== null) { + $this->offset = $query->offset; + } + + if ($query->where !== null) { + $this->andWhere($query->where); + } + + if ($query->having !== null) { + $this->andHaving($query->having); + } + + if ($query->params !== null) { + $this->addParams($query->params); + } + + if ($query->orderBy !== null) { + $this->addOrderBy($query->orderBy); + } + + if ($query->groupBy !== null) { + $this->addGroupBy($query->groupBy); + } + + if ($query->join !== null) { + if (empty($this->join)) { + $this->join = $query->join; + } else { + if (!is_array($this->join)) { + $this->join = array($this->join); + } + if (is_array($query->join)) { + $this->join = array_merge($this->join, $query->join); + } else { + $this->join[] = $query->join; + } + } + } + + if ($query->union !== null) { + if (empty($this->union)) { + $this->union = $query->union; + } else { + if (!is_array($this->union)) { + $this->union = array($this->union); + } + if (is_array($query->union)) { + $this->union = array_merge($this->union, $query->union); + } else { + $this->union[] = $query->union; + } + } + } + + return $this; + } +} diff --git a/framework/db/dao/Command.php b/framework/db/dao/Command.php index 6da71fb..06558f3 100644 --- a/framework/db/dao/Command.php +++ b/framework/db/dao/Command.php @@ -64,27 +64,15 @@ class Command extends \yii\base\Component /** * Constructor. - * Instead of using the `new` operator, you may use [[Connection::createCommand()]] - * to create a new Command object. * @param Connection $connection the database connection - * @param string|array|Query $query the DB query to be executed. This can be: - * - * - a string representing the SQL statement to be executed - * - a [[Query]] object representing the SQL query - * - an array that will be used to create and initialize the [[Query]] object + * @param string $sql the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement */ - public function __construct($connection, $query = null) + public function __construct($connection, $sql = null, $params = array()) { $this->connection = $connection; - if (is_array($query)) { - $query = Query::newInstance($query); - } - if ($query instanceof Query) { - $this->_sql = $query->getSql($connection); - $this->bindValues($query->params); - } else { - $this->_sql = $query; - } + $this->_sql = $sql; + $this->bindValues($params); } /** @@ -233,8 +221,6 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } -echo "Executing SQL: {$sql}{$paramLog}" . "\n\n"; - \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); try { @@ -368,8 +354,6 @@ echo "Executing SQL: {$sql}{$paramLog}" . "\n\n"; $paramLog = "\nParameters: " . var_export($this->_params, true); } -echo "Executing SQL: {$sql}{$paramLog}" . "\n\n"; - \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); if ($db->queryCachingCount > 0 && $db->queryCachingDuration >= 0 && $method !== '') { diff --git a/framework/db/dao/Connection.php b/framework/db/dao/Connection.php index 482b152..f2e5445 100644 --- a/framework/db/dao/Connection.php +++ b/framework/db/dao/Connection.php @@ -399,17 +399,14 @@ class Connection extends \yii\base\ApplicationComponent /** * Creates a command for execution. - * @param string|array|Query $query the DB query to be executed. This can be: - * - * - a string representing the SQL statement to be executed - * - a [[Query]] object representing the SQL query - * - an array that will be used to initialize [[Query]] + * @param string $sql the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement * @return Command the DB command */ - public function createCommand($query = null) + public function createCommand($sql = null, $params = array()) { $this->open(); - return new Command($this, $query); + return new Command($this, $sql, $params); } /** diff --git a/framework/db/dao/Query.php b/framework/db/dao/Query.php index 16bf1e2..aaf1a61 100644 --- a/framework/db/dao/Query.php +++ b/framework/db/dao/Query.php @@ -39,7 +39,7 @@ namespace yii\db\dao; * @author Qiang Xue * @since 2.0 */ -class Query extends \yii\base\Object +class Query extends BaseQuery { /** * @var array the operation that this query represents. This refers to the method call as well as @@ -48,80 +48,11 @@ class Query extends \yii\base\Object * If this property is not set, it means this query represents a SELECT statement. */ public $operation; - /** - * @var string|array the columns being selected. This refers to the SELECT clause in a SQL - * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). - * If not set, if means all columns. - * @see select() - */ - public $select; - /** - * @var string additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - */ - public $selectOption; - /** - * @var boolean whether to select distinct rows of data only. If this is set true, - * the SELECT clause would be changed to SELECT DISTINCT. - */ - public $distinct; - /** - * @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. - * It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). - * @see from() - */ - public $from; - /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. - */ - public $offset; - /** - * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. - * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). - */ - public $orderBy; - /** - * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. - * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). - */ - public $groupBy; - /** - * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. - * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. - * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). - * @see join() - */ - public $join; - /** - * @var string|array the condition to be applied in the GROUP BY clause. - * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. - */ - public $having; - /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - */ - public $params; - /** - * @var string|Query[] the UNION clause(s) in a SQL statement. This can be either a string - * representing a single UNION clause or an array representing multiple UNION clauses. - * Each union clause can be a string or a `Query` object which refers to the SQL statement. - */ - public $union; /** * Generates and returns the SQL statement according to this query. + * Note that after calling this method, [[params]] may be modified with additional + * parameters generated by the query builder. * @param Connection $connection the database connection used to generate the SQL statement. * If this parameter is not given, the `db` application component will be used. * @return string the generated SQL statement @@ -131,7 +62,15 @@ class Query extends \yii\base\Object if ($connection === null) { $connection = \Yii::$application->db; } - return $connection->getQueryBuilder()->build($this); + $qb = $connection->getQueryBuilder(); + if ($this->operation !== null) { + $params = $this->operation; + $method = array_shift($params); + $qb->query = $this; + return call_user_func_array(array($qb, $method), $params); + } else { + return $qb->build($this); + } } /** @@ -145,7 +84,8 @@ class Query extends \yii\base\Object if ($connection === null) { $connection = \Yii::$application->db; } - return $connection->createCommand($this); + $sql = $this->getSql($connection); + return $connection->createCommand($sql, $this->params); } /** @@ -157,7 +97,7 @@ class Query extends \yii\base\Object */ public function insert($table, $columns) { - $this->operation = array(__FUNCTION__, $table, $columns, array()); + $this->operation = array(__FUNCTION__, $table, $columns); return $this; } @@ -173,8 +113,7 @@ class Query extends \yii\base\Object */ public function update($table, $columns, $condition = '', $params = array()) { - $this->addParams($params); - $this->operation = array(__FUNCTION__, $table, $columns, $condition, array()); + $this->operation = array(__FUNCTION__, $table, $columns, $condition, $params); return $this; } @@ -188,8 +127,8 @@ class Query extends \yii\base\Object */ public function delete($table, $condition = '', $params = array()) { - $this->operation = array(__FUNCTION__, $table, $condition); - return $this->addParams($params); + $this->operation = array(__FUNCTION__, $table, $condition, $params); + return $this; } /** @@ -361,559 +300,4 @@ class Query extends \yii\base\Object $this->operation = array(__FUNCTION__, $name, $table); return $this; } - - /** - * Sets the SELECT part of the query. - * @param string|array $columns the columns to be selected. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - * @return Query the query object itself - */ - public function select($columns, $option = '') - { - $this->select = $columns; - $this->selectOption = $option; - return $this; - } - - /** - * Sets the value indicating whether to SELECT DISTINCT or not. - * @param bool $value whether to SELECT DISTINCT or not. - * @return Query the query object itself - */ - public function distinct($value = true) - { - $this->distinct = $value; - return $this; - } - - /** - * Sets the FROM part of the query. - * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. 'tbl_user') - * or an array (e.g. array('tbl_user', 'tbl_profile')) specifying one or several table names. - * Table names can contain schema prefixes (e.g. 'public.tbl_user') and/or table aliases (e.g. 'tbl_user u'). - * The method will automatically quote the table names unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @return Query the query object itself - */ - public function from($tables) - { - $this->from = $tables; - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter, and optionally a $params parameter - * specifying the values to be bound to the query. - * - * The $condition parameter should be either a string (e.g. 'id=1') or an array. - * If the latter, it must be in one of the following two formats: - * - * - hash format: `array('column1' => value1, 'column2' => value2, ...)` - * - operator format: `array(operator, operand1, operand2, ...)` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `array('type'=>1, 'status'=>2)` generates `(type=1) AND (status=2)`. - * - `array('id'=>array(1,2,3), 'status'=>2)` generates `(id IN (1,2,3)) AND (status=2)`. - * - `array('status'=>null) generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `array('and', 'id=1', 'id=2')` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `array('and', 'type=1', array('or', 'id=1', 'id=2'))` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `array('between', 'id', 1, 10)` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `array('in', 'id', array(1,2,3))` will generate `id IN (1,2,3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `array('like', 'name', '%tester%')` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `array('like', 'name', array('%test%', '%sample%'))` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape values in the range. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * @param string|array $condition the conditions that should be put in the WHERE part. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition, $params = array()) - { - $this->where = $condition; - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition, $params = array()) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('and', $this->where, $condition); - } - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition, $params = array()) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('or', $this->where, $condition); - } - $this->addParams($params); - return $this; - } - - /** - * Appends a JOIN part to the query. - * The first parameter specifies what type of join it is. - * @param string $type the type of join, such as INNER JOIN, LEFT JOIN. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - */ - public function join($type, $table, $on = '', $params = array()) - { - $this->join[] = array($type, $table, $on); - return $this->addParams($params); - } - - /** - * Appends an INNER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - */ - public function innerJoin($table, $on = '', $params = array()) - { - $this->join[] = array('INNER JOIN', $table, $on); - return $this->addParams($params); - } - - /** - * Appends a LEFT OUTER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query - * @return Query the query object itself - */ - public function leftJoin($table, $on = '', $params = array()) - { - $this->join[] = array('LEFT JOIN', $table, $on); - return $this->addParams($params); - } - - /** - * Appends a RIGHT OUTER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query - * @return Query the query object itself - */ - public function rightJoin($table, $on = '', $params = array()) - { - $this->join[] = array('RIGHT JOIN', $table, $on); - return $this->addParams($params); - } - - /** - * Sets the GROUP BY part of the query. - * @param string|array $columns the columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return Query the query object itself - * @see addGroupBy() - */ - public function groupBy($columns) - { - $this->groupBy = $columns; - return $this; - } - - /** - * Adds additional group-by columns to the existing ones. - * @param string|array $columns additional columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return Query the query object itself - * @see groupBy() - */ - public function addGroupBy($columns) - { - if (empty($this->groupBy)) { - $this->groupBy = $columns; - } else { - if (!is_array($this->groupBy)) { - $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->groupBy = array_merge($this->groupBy, $columns); - } - return $this; - } - - /** - * Sets the HAVING part of the query. - * @param string|array $condition the conditions to be put after HAVING. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see andHaving() - * @see orHaving() - */ - public function having($condition, $params = array()) - { - $this->having = $condition; - $this->addParams($params); - return $this; - } - - /** - * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see having() - * @see orHaving() - */ - public function andHaving($condition, $params = array()) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = array('and', $this->having, $condition); - } - $this->addParams($params); - return $this; - } - - /** - * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return Query the query object itself - * @see having() - * @see andHaving() - */ - public function orHaving($condition, $params = array()) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = array('or', $this->having, $condition); - } - $this->addParams($params); - return $this; - } - - /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return Query the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->orderBy = $columns; - return $this; - } - - /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return Query the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - if (empty($this->orderBy)) { - $this->orderBy = $columns; - } else { - if (!is_array($this->orderBy)) { - $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit - * @return Query the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset - * @return Query the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } - - /** - * Appends a SQL statement using UNION operator. - * @param string|Query $sql the SQL statement to be appended using UNION - * @return Query the query object itself - */ - public function union($sql) - { - $this->union[] = $sql; - return $this; - } - - /** - * Sets the parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * @return Query the query object itself - * @see addParams() - */ - public function params($params) - { - $this->params = $params; - return $this; - } - - /** - * Adds additional parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * @return Query the query object itself - * @see params() - */ - public function addParams($params) - { - if ($params !== array()) { - if ($this->params === null) { - $this->params = $params; - } else { - foreach ($params as $name => $value) { - if (is_integer($name)) { - $this->params[] = $value; - } else { - $this->params[$name] = $value; - } - } - } - } - return $this; - } - - /** - * Merges this query with another one. - * - * The merging is done according to the following rules: - * - * - [[select]]: the union of both queries' [[select]] property values. - * - [[selectOption]], [[distinct]], [[from]], [[limit]], [[offset]]: the new query - * takes precedence over this query. - * - [[where]], [[having]]: the new query's corresponding property value - * will be 'AND' together with the existing one. - * - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's - * corresponding property value will be appended to the existing one. - * - * In general, the merging makes the resulting query more restrictive and specific. - * @param Query $query the new query to be merged with this query. - * @return Query the query object itself - */ - public function mergeWith($query) - { - if ($this->select !== $query->select) { - if (empty($this->select)) { - $this->select = $query->select; - } elseif (!empty($query->select)) { - $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; - $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; - $this->select = array_merge($select1, array_diff($select2, $select1)); - } - } - - if ($query->selectOption !== null) { - $this->selectOption = $query->selectOption; - } - - if ($query->distinct !== null) { - $this->distinct = $query->distinct; - } - - if ($query->from !== null) { - $this->from = $query->from; - } - - if ($query->limit !== null) { - $this->limit = $query->limit; - } - - if ($query->offset !== null) { - $this->offset = $query->offset; - } - - if ($query->where !== null) { - $this->andWhere($query->where); - } - - if ($query->having !== null) { - $this->andHaving($query->having); - } - - if ($query->params !== null) { - $this->addParams($query->params); - } - - if ($query->orderBy !== null) { - $this->addOrderBy($query->orderBy); - } - - if ($query->groupBy !== null) { - $this->addGroupBy($query->groupBy); - } - - if ($query->join !== null) { - if (empty($this->join)) { - $this->join = $query->join; - } else { - if (!is_array($this->join)) { - $this->join = array($this->join); - } - if (is_array($query->join)) { - $this->join = array_merge($this->join, $query->join); - } else { - $this->join[] = $query->join; - } - } - } - - if ($query->union !== null) { - if (empty($this->union)) { - $this->union = $query->union; - } else { - if (!is_array($this->union)) { - $this->union = array($this->union); - } - if (is_array($query->union)) { - $this->union = array_merge($this->union, $query->union); - } else { - $this->union[] = $query->union; - } - } - } - - return $this; - } - - /** - * Resets the query object to its original state. - * @return Query the query object itself - */ - public function reset() - { - foreach (get_object_vars($this) as $name => $value) { - $this->$name = null; - } - return $this; - } } diff --git a/framework/db/dao/QueryBuilder.php b/framework/db/dao/QueryBuilder.php index 2ca4575..3e8893d 100644 --- a/framework/db/dao/QueryBuilder.php +++ b/framework/db/dao/QueryBuilder.php @@ -13,9 +13,9 @@ namespace yii\db\dao; use yii\db\Exception; /** - * QueryBuilder builds a SQL statement based on the specification given as a [[Query]] object. + * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[BaseQuery]] object. * - * QueryBuilder is often used behind the scenes by [[Query]] to build a DBMS-dependent SQL statement + * QueryBuilder can also be used to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE, * from a [[Query]] object. * * @author Qiang Xue @@ -57,35 +57,25 @@ class QueryBuilder extends \yii\base\Object } /** - * Generates a SQL statement from a [[Query]] object. - * Note that when generating SQL statements for INSERT and UPDATE queries, - * the query object's [[Query::params]] property may be appended with new parameters. - * @param Query $query the [[Query]] object from which the SQL statement will be generated + * Generates a SELECT SQL statement from a [[BaseQuery]] object. + * @param BaseQuery $query the [[Query]] object from which the SQL statement will be generated * @return string the generated SQL statement */ public function build($query) { $this->query = $query; - if ($query->operation !== null) { - // non-SELECT query - $params = $query->operation; - $method = array_shift($params); - return call_user_func_array(array($this, $method), $params); - } else { - // SELECT query - $clauses = array( - $this->buildSelect(), - $this->buildFrom(), - $this->buildJoin(), - $this->buildWhere(), - $this->buildGroupBy(), - $this->buildHaving(), - $this->buildUnion(), - $this->buildOrderBy(), - $this->buildLimit(), - ); - return implode($this->separator, array_filter($clauses)); - } + $clauses = array( + $this->buildSelect(), + $this->buildFrom(), + $this->buildJoin(), + $this->buildWhere(), + $this->buildGroupBy(), + $this->buildHaving(), + $this->buildUnion(), + $this->buildOrderBy(), + $this->buildLimit(), + ); + return implode($this->separator, array_filter($clauses)); } /** @@ -123,7 +113,7 @@ class QueryBuilder extends \yii\base\Object $count++; } } - if ($this->query instanceof Query) { + if ($this->query instanceof BaseQuery) { $this->query->addParams($params); } @@ -167,7 +157,7 @@ class QueryBuilder extends \yii\base\Object $count++; } } - if ($this->query instanceof Query) { + if ($this->query instanceof BaseQuery) { $this->query->addParams($params); } $sql = 'UPDATE ' . $this->quoteTableName($table) . ' SET ' . implode(', ', $lines); @@ -189,14 +179,18 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table where the data will be deleted from. * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the query. * @return integer number of rows affected by the execution. */ - public function delete($table, $condition = '') + public function delete($table, $condition = '', $params = array()) { $sql = 'DELETE FROM ' . $this->quoteTableName($table); if (($where = $this->buildCondition($condition)) != '') { $sql .= ' WHERE ' . $where; } + if ($params !== array() && $this->query instanceof BaseQuery) { + $this->query->addParams($params); + } return $sql; } @@ -631,7 +625,7 @@ class QueryBuilder extends \yii\base\Object protected function buildSelect() { $select = $this->query->distinct ? 'SELECT DISTINCT' : 'SELECT'; - if ($this->query->selectOption != '') { + if ($this->query->selectOption !== null) { $select .= ' ' . $this->query->selectOption; }