diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index a35f932..e7d58be 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -66,27 +66,38 @@ class ActiveQuery extends BaseQuery { $command = $this->createCommand(); $rows = $command->queryAll(); + if ($rows === array()) { + return array(); + } $models = $this->createModels($rows); - if (empty($this->with)) { - return $models; + if (!empty($this->with)) { + $this->fetchRelatedModels($models, $this->with); } + return $models; } /** * Executes query and returns a single row of result. - * @return ActiveRecord|array|boolean a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. False will be returned + * @return ActiveRecord|array|null a 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() { $command = $this->createCommand(); $row = $command->queryRow(); - if ($row === false || $this->asArray) { + if ($row === false) { + return false; + } elseif ($this->asArray) { return $row; } else { + /** @var $class ActiveRecord */ $class = $this->modelClass; - return $class::create($row); + $model = $class::create($row); + if (!empty($this->with)) { + $this->fetchRelatedModels(array($model), $this->with); + } + return $model; } } @@ -107,7 +118,8 @@ class ActiveQuery extends BaseQuery */ public function exists() { - return $this->select(array(new Expression('1')))->value() !== false; + $this->select = array(new Expression('1')); + return $this->value() !== false; } /** @@ -118,6 +130,7 @@ class ActiveQuery extends BaseQuery */ public function createCommand($db = null) { + /** @var $modelClass ActiveRecord */ $modelClass = $this->modelClass; if ($db === null) { $db = $modelClass::getDbConnection(); @@ -139,10 +152,9 @@ class ActiveQuery extends BaseQuery } /** @var $qb QueryBuilder */ $qb = $db->getQueryBuilder(); - return $db->createCommand($qb->build($this), $this->params); - } else { - return $db->createCommand($this->sql, $this->params); + $this->sql = $qb->build($this); } + return $db->createCommand($this->sql, $this->params); } public function asArray($value = true) @@ -173,55 +185,6 @@ class ActiveQuery extends BaseQuery return $this; } - protected function find() - { - $modelClass = $this->modelClass; - /** - * @var \yii\db\dao\Connection $db - */ - $db = $modelClass::getDbConnection(); - if ($this->sql === null) { - if ($this->from === null) { - $tableName = $modelClass::getTableSchema()->name; - $this->from = array($tableName); - } - foreach ($this->scopes as $name => $config) { - if (is_integer($name)) { - $modelClass::$config($this); - } else { - array_unshift($config, $this); - call_user_func_array(array($modelClass, $name), $config); - } - } - $this->sql = $db->getQueryBuilder()->build($this); - } - $command = $db->createCommand($this->sql, $this->params); - $rows = $command->queryAll(); - $records = $this->createRecords($rows); - if ($records !== array()) { - foreach ($this->with as $name => $config) { - /** @var Relation $relation */ - $relation = $model->$name(); - foreach ($config as $p => $v) { - $relation->$p = $v; - } - if ($relation->asArray === null) { - // inherit asArray from parent query - $relation->asArray = $this->asArray; - } - $rs = $relation->findWith($records); - /* - foreach ($rs as $r) { - // find the matching parent record(s) - // insert into the parent records(s) - } - */ - } - } - - return $records; - } - protected function createModels($rows) { $models = array(); @@ -248,4 +211,26 @@ class ActiveQuery extends BaseQuery } return $models; } + + protected function fetchRelatedModels(&$models, $relations) + { + // todo: normalize $relations + $primaryModel = new $this->modelClass; + foreach ($relations as $name => $properties) { + if (!method_exists($primaryModel, $name)) { + throw new Exception("Unknown relation: $name"); + } + /** @var $relation ActiveRelation */ + $relation = $primaryModel->$name(); + $relation->primaryModel = null; + foreach ($properties as $p => $v) { + $relation->$p = $v; + } + if ($relation->asArray === null) { + // inherit asArray from primary query + $relation->asArray = $this->asArray; + } + $relation->findWith($name, $models); + } + } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index fccfb98..70566a9 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -101,9 +101,9 @@ abstract class ActiveRecord extends Model * corresponding record. * - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object. * - * @return ActiveQuery|ActiveRecord|null the [[ActiveQuery]] instance for query purpose, or + * @return ActiveQuery|ActiveRecord|boolean the [[ActiveQuery]] instance for query purpose, or * the ActiveRecord object when a scalar is passed to this method which is considered to be a - * primary key value (null will be returned if no record is found in this case.) + * primary key value (false will be returned if no record is found in this case.) */ public static function find($q = null) { diff --git a/framework/db/ar/ActiveRelation.php b/framework/db/ar/ActiveRelation.php index 22986c1..f2cd7cf 100644 --- a/framework/db/ar/ActiveRelation.php +++ b/framework/db/ar/ActiveRelation.php @@ -19,22 +19,18 @@ namespace yii\db\ar; * @author Qiang Xue * @since 2.0 */ -class ActiveRelation extends BaseActiveQuery +class ActiveRelation extends ActiveQuery { /** - * @var string the class name of the ActiveRecord instances that this relation - * should create and populate query results into - */ - public $modelClass; - /** - * @var ActiveRecord the primary record that this relation is associated with. + * @var ActiveRecord the primary model that this relation is associated with. * This is used only in lazy loading with dynamic query options. */ public $primaryModel; /** - * @var boolean whether this relation is a one-many relation + * @var boolean whether this relation should populate all query results into AR instances. + * If false, only the first row of the results will be taken. */ - public $hasMany; + public $multiple; /** * @var array the columns of the primary and foreign tables that establish the relation. * The array keys must be columns of the table for this relation, and the array values @@ -47,78 +43,85 @@ class ActiveRelation extends BaseActiveQuery */ public $via; - public function one() - { - $models = $this->all(); - return isset($models[0]) ? $models[0] : null; - } - - public function all() + public function createCommand($db = null) { - $models = array(); - return $models; + if ($this->primaryModel !== null) { + $this->filterByPrimaryModels(array($this->primaryModel)); + } + return parent::createCommand($db); } - public function findWith($name, &$primaryRecords) + public function findWith($name, &$primaryModels) { if (empty($this->link) || !is_array($this->link)) { throw new \yii\base\Exception('invalid link'); } - $this->addLinkCondition($primaryRecords); - $records = $this->find(); + $this->filterByPrimaryModels($primaryModels); - /** @var array $map mapping key(s) to index of $primaryRecords */ - $index = $this->buildRecordIndex($primaryRecords, array_values($this->link)); - $this->initRecordRelation($primaryRecords, $name); - foreach ($records as $record) { - $key = $this->getRecordKey($record, array_keys($this->link)); - if (isset($index[$key])) { - $primaryRecords[$map[$key]][$name] = $record; + if (count($primaryModels) === 1 && !$this->multiple) { + foreach ($primaryModels as $i => $primaryModel) { + $primaryModels[$i][$name] = $this->one(); + } + } else { + $models = $this->all(); + // distribute models into buckets which are indexed by the link keys + $buckets = array(); + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, array_keys($this->link)); + if ($this->index !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + if (!$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, array_values($this->link)); + if (isset($buckets[$key])) { + $primaryModels[$i][$name] = $buckets[$key]; + } else { + $primaryModels[$i][$name] = $this->multiple ? array() : null; + } } } } - protected function getRecordKey($record, $attributes) + protected function getModelKey($model, $attributes) { - if (isset($attributes[1])) { + if (count($attributes) > 1) { $key = array(); foreach ($attributes as $attribute) { - $key[] = is_array($record) ? $record[$attribute] : $record->$attribute; + $key[] = $model[$attribute]; } return serialize($key); } else { - $attribute = $attributes[0]; - return is_array($record) ? $record[$attribute] : $record->$attribute; - } - } - - protected function buildRecordIndex($records, $attributes) - { - $map = array(); - foreach ($records as $i => $record) { - $map[$this->getRecordKey($record, $attributes)] = $i; + $attribute = reset($attributes); + return $model[$attribute]; } - return $map; } - protected function addLinkCondition($primaryRecords) + protected function filterByPrimaryModels($primaryModels) { $attributes = array_keys($this->link); $values = array(); if (isset($links[1])) { // composite keys - foreach ($primaryRecords as $record) { + foreach ($primaryModels as $model) { $v = array(); foreach ($this->link as $attribute => $link) { - $v[$attribute] = is_array($record) ? $record[$link] : $record->$link; + $v[$attribute] = is_array($model) ? $model[$link] : $model->$link; } $values[] = $v; } } else { // single key $attribute = $this->link[$links[0]]; - foreach ($primaryRecords as $record) { - $values[] = is_array($record) ? $record[$attribute] : $record->$attribute; + foreach ($primaryModels as $model) { + $values[] = is_array($model) ? $model[$attribute] : $model->$attribute; } } $this->andWhere(array('in', $attributes, $values)); diff --git a/framework/db/dao/BaseQuery.php b/framework/db/dao/BaseQuery.php index aec115f..f91beb7 100644 --- a/framework/db/dao/BaseQuery.php +++ b/framework/db/dao/BaseQuery.php @@ -544,6 +544,7 @@ class BaseQuery extends \yii\base\Component 'where', 'limit', 'offset', 'orderBy', 'groupBy', 'join', 'having', 'union', 'params', ); + // todo: incorrect, do we need it? should we provide a configure() method instead? foreach ($properties as $name => $value) { if ($value !== null) { $this->$name = $value;