diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index f82bf2e..808ab95 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -232,14 +232,7 @@ class ActiveQuery extends BaseQuery // inherit asArray from primary query $relation->asArray = $this->asArray; } - if ($relation->via !== null) { - $viaName = $relation->via; - $viaQuery = $primaryModel->$viaName(); - $viaQuery->primaryModel = null; - $relation->findWith($name, $models, $viaQuery); - } else { - $relation->findWith($name, $models); - } + $relation->findWith($name, $models); } } } diff --git a/framework/db/ar/ActiveQueryBuilder.php b/framework/db/ar/ActiveQueryBuilder.php deleted file mode 100644 index 620c336..0000000 --- a/framework/db/ar/ActiveQueryBuilder.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @since 2.0 - */ -class ActiveQueryBuilder extends \yii\base\Object -{ - /** - * @var \yii\db\dao\QueryBuilder - */ - public $queryBuilder; - /** - * @var ActiveQuery - */ - public $query; - - public function __construct($query, $config = array()) - { - $this->query = $query; - parent::__construct($config); - } - - public function build() - { - - } -} \ No newline at end of file diff --git a/framework/db/ar/ActiveRelation.php b/framework/db/ar/ActiveRelation.php index f6e7f2e..c1f6a10 100644 --- a/framework/db/ar/ActiveRelation.php +++ b/framework/db/ar/ActiveRelation.php @@ -10,6 +10,10 @@ namespace yii\db\ar; +use yii\db\dao\Connection; +use yii\db\dao\Command; +use yii\db\dao\QueryBuilder; + /** * It is used in three scenarios: * - eager loading: User::find()->with('posts')->all(); @@ -22,50 +26,90 @@ namespace yii\db\ar; class ActiveRelation extends ActiveQuery { /** - * @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 should populate all query results into AR instances. * If false, only the first row of the results will be taken. */ public $multiple; /** + * @var ActiveRecord the primary model that this relation is associated with. + * This is used only in lazy loading with dynamic query options. + */ + protected $primaryModel; + /** * @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 * must be the corresponding columns from the primary table. * Do not prefix or quote the column names as they will be done automatically by Yii. */ - public $link; + protected $link; /** - * @var array + * @var array|ActiveRelation */ - public $via; + protected $via; + /** - * @var array + * @param string $relationName + * @param array|\Closure $options + * @return ActiveRelation */ - public $viaTable; - - public function via($modelClass, $properties = array()) + public function via($relationName, $options = null) { - $this->via = $modelClass; + /** @var $relation ActiveRelation */ + $relation = $this->primaryModel->$relationName(); + $relation->primaryModel = null; + $this->via = array($relationName, $relation); + if (is_array($options)) { + foreach ($options as $name => $value) { + $this->$name = $value; + } + } elseif ($options instanceof \Closure) { + $options($relation); + } return $this; } - public function viaTable($tableName, $link, $properties = array()) + /** + * @param string $tableName + * @param array $link + * @param array|\Closure $options + * @return ActiveRelation + */ + public function viaTable($tableName, $link, $options = null) { - $this->viaTable = array($tableName, $link, $properties); + $relation = new ActiveRelation(array( + 'modelClass' => get_class($this->primaryModel), + 'from' => array($tableName), + 'link' => $link, + 'multiple' => true, + 'asArray' => true, + )); + $this->via = $relation; + if (is_array($options)) { + foreach ($options as $name => $value) { + $this->$name = $value; + } + } elseif ($options instanceof \Closure) { + $options($relation); + } return $this; } + /** + * Creates a DB command that can be used to execute this query. + * @return Command the created DB command instance. + */ public function createCommand() { if ($this->primaryModel !== null) { - if ($this->via !== null) { - /** @var $viaQuery ActiveRelation */ - $viaName = $this->via; - $viaModels = $this->primaryModel->$viaName; + // lazy loading + if ($this->via instanceof self) { + // via pivot table + $viaModels = $this->via->findPivotRows(array($this->primaryModel)); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + $relationName = $this->via[0]; + $viaModels = $this->primaryModel->$relationName; if ($viaModels === null) { $viaModels = array(); } elseif (!is_array($viaModels)) { @@ -79,14 +123,23 @@ class ActiveRelation extends ActiveQuery return parent::createCommand(); } - public function findWith($name, &$primaryModels, $viaQuery = null) + public function findWith($name, &$primaryModels) { if (!is_array($this->link)) { throw new \yii\base\Exception('invalid link'); } - if ($viaQuery !== null) { - $viaModels = $viaQuery->findWith($this->via, $primaryModels); + if ($this->via instanceof self) { + // via pivot table + /** @var $viaQuery ActiveRelation */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var $viaQuery ActiveRelation */ + list($viaName, $viaQuery) = $this->via; + $viaModels = $viaQuery->findWith($viaName, $primaryModels); $this->filterByModels($viaModels); } else { $this->filterByModels($primaryModels); @@ -106,12 +159,9 @@ class ActiveRelation extends ActiveQuery $buckets = $this->buildBuckets($models, $this->link); } + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); foreach ($primaryModels as $i => $primaryModel) { - if (isset($viaQuery)) { - $key = $this->getModelKey($primaryModel, array_values($viaQuery->link)); - } else { - $key = $this->getModelKey($primaryModel, array_values($this->link)); - } + $key = $this->getModelKey($primaryModel, $link); if (isset($buckets[$key])) { $primaryModels[$i][$name] = $buckets[$key]; } else { @@ -125,8 +175,9 @@ class ActiveRelation extends ActiveQuery protected function buildBuckets($models, $link, $viaModels = null, $viaLink = null) { $buckets = array(); + $linkKeys = array_keys($link); foreach ($models as $i => $model) { - $key = $this->getModelKey($model, array_keys($link)); + $key = $this->getModelKey($model, $linkKeys); if ($this->index !== null) { $buckets[$key][$i] = $model; } else { @@ -136,9 +187,11 @@ class ActiveRelation extends ActiveQuery if ($viaModels !== null) { $viaBuckets = array(); + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, array_keys($viaLink)); - $key2 = $this->getModelKey($viaModel, array_values($link)); + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); if (isset($buckets[$key2])) { foreach ($buckets[$key2] as $i => $bucket) { if ($this->index !== null) { @@ -194,7 +247,23 @@ class ActiveRelation extends ActiveQuery $values[] = $v; } } - $this->andWhere(array('in', $attributes, $values)); + $this->andWhere(array('in', $attributes, array_unique($values, SORT_REGULAR))); } + /** + * @param ActiveRecord[] $primaryModels + * @return array + */ + protected function findPivotRows($primaryModels) + { + if (empty($primaryModels)) { + return array(); + } + $this->filterByModels($primaryModels); + /** @var $primaryModel ActiveRecord */ + $primaryModel = reset($primaryModels); + $db = $primaryModel->getDbConnection(); + $sql = $db->getQueryBuilder()->build($this); + return $db->createCommand($sql, $this->params)->queryAll(); + } } diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index df28a38..ee28e18 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -16,112 +16,112 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase ActiveRecord::$db = $this->getConnection(); } -// public function testFind() -// { -// // find one -// $result = Customer::find(); -// $this->assertTrue($result instanceof ActiveQuery); -// $customer = $result->one(); -// $this->assertTrue($customer instanceof Customer); -// -// // find all -// $result = Customer::find(); -// $customers = $result->all(); -// $this->assertEquals(3, count($customers)); -// $this->assertTrue($customers[0] instanceof Customer); -// $this->assertTrue($customers[1] instanceof Customer); -// $this->assertTrue($customers[2] instanceof Customer); -// -// // find by a single primary key -// $customer = Customer::find(2); -// $this->assertTrue($customer instanceof Customer); -// $this->assertEquals('user2', $customer->name); -// -// // find by attributes -// $customer = Customer::find()->where(array('name' => 'user2'))->one(); -// $this->assertTrue($customer instanceof Customer); -// $this->assertEquals(2, $customer->id); -// -// // find by Query array -// $query = array( -// 'where' => 'id=:id', -// 'params' => array(':id' => 2), -// ); -// $customer = Customer::find($query)->one(); -// $this->assertTrue($customer instanceof Customer); -// $this->assertEquals('user2', $customer->name); -// -// // find count -// $this->assertEquals(3, Customer::count()->value()); -// $this->assertEquals(2, Customer::count(array( -// 'where' => 'id=1 OR id=2', -// ))->value()); -// $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value()); -// } -// -// public function testFindBySql() -// { -// // find one -// $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); -// $this->assertTrue($customer instanceof Customer); -// $this->assertEquals('user3', $customer->name); -// -// // find all -// $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); -// $this->assertEquals(3, count($customers)); -// -// // find with parameter binding -// $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); -// $this->assertTrue($customer instanceof Customer); -// $this->assertEquals('user2', $customer->name); -// } -// -// public function testScope() -// { -// $customers = Customer::find(array( -// 'scopes' => array('active'), -// ))->all(); -// $this->assertEquals(2, count($customers)); -// -// $customers = Customer::find()->active()->all(); -// $this->assertEquals(2, count($customers)); -// } -// -// public function testFindLazy() -// { -// /** @var $customer Customer */ -// $customer = Customer::find(2); -// $orders = $customer->orders; -// $this->assertEquals(2, count($orders)); -// -// $orders = $customer->orders()->where('id=3')->all(); -// $this->assertEquals(1, count($orders)); -// $this->assertEquals(3, $orders[0]->id); -// } -// -// public function testFindEager() -// { -// $customers = Customer::find()->with('orders')->all(); -// $this->assertEquals(3, count($customers)); -// $this->assertEquals(1, count($customers[0]->orders)); -// $this->assertEquals(2, count($customers[1]->orders)); -// } -// -// public function testFindLazyVia() -// { -// /** @var $order Order */ -// $order = Order::find(1); -// $this->assertEquals(1, $order->id); -// $this->assertEquals(2, count($order->items)); -// $this->assertEquals(1, $order->items[0]->id); -// $this->assertEquals(2, $order->items[1]->id); -// -// $order = Order::find(1); -// $order->id = 100; -// $this->assertEquals(array(), $order->items); -// } + public function testFind() + { + // find one + $result = Customer::find(); + $this->assertTrue($result instanceof ActiveQuery); + $customer = $result->one(); + $this->assertTrue($customer instanceof Customer); + + // find all + $result = Customer::find(); + $customers = $result->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[0] instanceof Customer); + $this->assertTrue($customers[1] instanceof Customer); + $this->assertTrue($customers[2] instanceof Customer); + + // find by a single primary key + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + + // find by attributes + $customer = Customer::find()->where(array('name' => 'user2'))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(2, $customer->id); + + // find by Query array + $query = array( + 'where' => 'id=:id', + 'params' => array(':id' => 2), + ); + $customer = Customer::find($query)->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + + // find count + $this->assertEquals(3, Customer::count()->value()); + $this->assertEquals(2, Customer::count(array( + 'where' => 'id=1 OR id=2', + ))->value()); + $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value()); + } - public function testFindEagerVia() + public function testFindBySql() + { + // find one + $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user3', $customer->name); + + // find all + $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + $this->assertEquals(3, count($customers)); + + // find with parameter binding + $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + } + + public function testScope() + { + $customers = Customer::find(array( + 'scopes' => array('active'), + ))->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->active()->all(); + $this->assertEquals(2, count($customers)); + } + + public function testFindLazy() + { + /** @var $customer Customer */ + $customer = Customer::find(2); + $orders = $customer->orders; + $this->assertEquals(2, count($orders)); + + $orders = $customer->orders()->where('id=3')->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(3, $orders[0]->id); + } + + public function testFindEager() + { + $customers = Customer::find()->with('orders')->all(); + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + } + + public function testFindLazyVia() + { + /** @var $order Order */ + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(1); + $order->id = 100; + $this->assertEquals(array(), $order->items); + } + + public function testFindEagerViaRelation() { $orders = Order::find()->with('items')->orderBy('id')->all(); $this->assertEquals(3, count($orders)); @@ -132,6 +132,41 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals(2, $order->items[1]->id); } + public function testFindLazyViaTable() + { + /** @var $order Order */ + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(2); + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + } + + public function testFindEagerViaTable() + { + $orders = Order::find()->with('books')->orderBy('id')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->books[0]->id); + $this->assertEquals(2, $order->books[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertEquals(1, count($order->books)); + $this->assertEquals(2, $order->books[0]->id); + } + // public function testInsert() // {