From c4c328dc92be780196c21be6adf1632e3d811acb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 6 Jan 2014 23:56:58 -0500 Subject: [PATCH] Fixes #1791: support ON condition for relational query. --- docs/guide/active-record.md | 37 ++++++++++- framework/yii/db/ActiveQuery.php | 5 +- framework/yii/db/ActiveRelation.php | 91 ++++++++++++++++++++-------- tests/unit/data/ar/Order.php | 7 +++ tests/unit/framework/db/ActiveRecordTest.php | 34 +++++++++++ 5 files changed, 147 insertions(+), 27 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index a414bce..44b8ecb 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -449,10 +449,45 @@ Below are some more examples, ```php // find all orders that contain books, but do not eager loading "books". $orders = Order::find()->innerJoinWith('books', false)->all(); -// equivalent to the above +// which is equivalent to the above $orders = Order::find()->joinWith('books', false, 'INNER JOIN')->all(); ``` +Sometimes when joining two tables, you may need to specify some extra condition in the ON part of the JOIN query. +This can be done by calling the [[\yii\db\ActiveRelation::onCondition()]] method like the following: + +```php +class User extends ActiveRecord +{ + public function getBooks() + { + return $this->hasMany(Item::className(), ['owner_id' => 'id']->onCondition(['category_id' => 1]); + } +} +``` + +In the above, the `hasMany()` method returns an `ActiveRelation` instance, upon which `onCondition()` is called +to specify that only items whose `category_id` is 1 should be returned. + +When you perform query using [[ActiveQuery::joinWith()|joinWith()]], the on-condition will be put in the ON part +of the corresponding JOIN query. For example, + +```php +// SELECT tbl_user.* FROM tbl_user LEFT JOIN tbl_item ON tbl_item.owner_id=tbl_user.id AND category_id=1 +// SELECT * FROM tbl_item WHERE owner_id IN (...) AND category_id=1 +$users = User::model()->joinWith('books')->all(); +``` + +Note that if you use eager loading via [[ActiveQuery::with()]] or lazy loading, the on-condition will be put +in the WHERE part of the corresponding SQL statement, because there is no JOIN query involved. For example, + +```php +// SELECT * FROM tbl_user WHERE id=10 +$user = User::model(10); +// SELECT * FROM tbl_item WHERE owner_id=10 AND category_id=1 +$books = $user->books; +``` + Working with Relationships -------------------------- diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 556f15f..068c713 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -389,8 +389,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]"; } $on = implode(' AND ', $on); + if (!empty($child->on)) { + $on = ['and', $on, $child->on]; + } } else { - $on = ''; + $on = $child->on; } $this->join($joinType, $childTable, $on); diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index 0659ee3..559bc28 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -29,6 +29,28 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface use ActiveRelationTrait; /** + * @var string|array the join condition. Please refer to [[Query::where()]] on how to specify this parameter. + * The condition will be used in the ON part when [[ActiveQuery::joinRelation()]] is called. + * Otherwise, the condition will be used in the WHERE part of a query. + */ + public $on; + + /** + * Sets the ON condition for the query. + * The condition will be used in the ON part when [[ActiveQuery::joinRelation()]] is called. + * Otherwise, the condition will be used in the WHERE part of a query. + * @param string|array $condition the ON condition. 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 static the query object itself + */ + public function onCondition($condition, $params = []) + { + $this->on = $condition; + $this->addParams($params); + return $this; + } + + /** * Specifies the pivot table. * @param string $tableName the name of the pivot table. * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. @@ -62,33 +84,52 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface */ public function createCommand($db = null) { - if ($this->primaryModel !== null) { - $where = $this->where; - // lazy loading - if ($this->via instanceof self) { - // via pivot table - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveRelation $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); + if ($this->primaryModel === null) { + // eager loading + if (!empty($this->on)) { + $where = $this->where; + $this->andWhere($this->on); + $command = parent::createCommand($db); + $this->where = $where; + return $command; + } else { + return parent::createCommand($db); + } + } + + // lazy loading + + $where = $this->where; + + if ($this->via instanceof self) { + // via pivot table + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveRelation $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); } else { - $this->filterByModels([$this->primaryModel]); + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; } - $command = parent::createCommand($db); - $this->where = $where; - return $command; + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); } - return parent::createCommand($db); + + if (!empty($this->on)) { + $this->andWhere($this->on); + } + + $command = parent::createCommand($db); + + $this->where = $where; + + return $command; } } diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 476db1f..bbbca52 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -58,6 +58,13 @@ class Order extends ActiveRecord ->where(['category_id' => 1]); } + public function getBooks2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->onCondition(['category_id' => 1]) + ->viaTable('tbl_order_item', ['order_id' => 'id']); + } + public function beforeSave($insert) { if (parent::beforeSave($insert)) { diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 0366fa9..37339dc 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -290,5 +290,39 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue($orders[0]->isRelationPopulated('customer')); $this->assertTrue($orders[1]->isRelationPopulated('customer')); $this->assertTrue($orders[2]->isRelationPopulated('customer')); + + // join with ON condition + $orders = Order::find()->joinWith('books2')->orderBy('tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books2')); + $this->assertTrue($orders[1]->isRelationPopulated('books2')); + $this->assertTrue($orders[2]->isRelationPopulated('books2')); + $this->assertEquals(2, count($orders[0]->books2)); + $this->assertEquals(0, count($orders[1]->books2)); + $this->assertEquals(1, count($orders[2]->books2)); + + // lazy loading with ON condition + $order = Order::find(1); + $this->assertEquals(2, count($order->books2)); + $order = Order::find(2); + $this->assertEquals(0, count($order->books2)); + $order = Order::find(3); + $this->assertEquals(1, count($order->books2)); + + // eager loading with ON condition + $orders = Order::find()->with('books2')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books2')); + $this->assertTrue($orders[1]->isRelationPopulated('books2')); + $this->assertTrue($orders[2]->isRelationPopulated('books2')); + $this->assertEquals(2, count($orders[0]->books2)); + $this->assertEquals(0, count($orders[1]->books2)); + $this->assertEquals(1, count($orders[2]->books2)); } }