Browse Source

Fixes #1791: support ON condition for relational query.

tags/2.0.0-beta
Qiang Xue 11 years ago
parent
commit
c4c328dc92
  1. 37
      docs/guide/active-record.md
  2. 5
      framework/yii/db/ActiveQuery.php
  3. 91
      framework/yii/db/ActiveRelation.php
  4. 7
      tests/unit/data/ar/Order.php
  5. 34
      tests/unit/framework/db/ActiveRecordTest.php

37
docs/guide/active-record.md

@ -449,10 +449,45 @@ Below are some more examples,
```php ```php
// find all orders that contain books, but do not eager loading "books". // find all orders that contain books, but do not eager loading "books".
$orders = Order::find()->innerJoinWith('books', false)->all(); $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(); $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 Working with Relationships
-------------------------- --------------------------

5
framework/yii/db/ActiveQuery.php

@ -389,8 +389,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]"; $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
} }
$on = implode(' AND ', $on); $on = implode(' AND ', $on);
if (!empty($child->on)) {
$on = ['and', $on, $child->on];
}
} else { } else {
$on = ''; $on = $child->on;
} }
$this->join($joinType, $childTable, $on); $this->join($joinType, $childTable, $on);

91
framework/yii/db/ActiveRelation.php

@ -29,6 +29,28 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
use ActiveRelationTrait; 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. * Specifies the pivot table.
* @param string $tableName the name of 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]]. * @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) public function createCommand($db = null)
{ {
if ($this->primaryModel !== null) { if ($this->primaryModel === null) {
$where = $this->where; // eager loading
// lazy loading if (!empty($this->on)) {
if ($this->via instanceof self) { $where = $this->where;
// via pivot table $this->andWhere($this->on);
$viaModels = $this->via->findPivotRows([$this->primaryModel]); $command = parent::createCommand($db);
$this->filterByModels($viaModels); $this->where = $where;
} elseif (is_array($this->via)) { return $command;
// via relation } else {
/** @var ActiveRelation $viaQuery */ return parent::createCommand($db);
list($viaName, $viaQuery) = $this->via; }
if ($viaQuery->multiple) { }
$viaModels = $viaQuery->all();
$this->primaryModel->populateRelation($viaName, $viaModels); // lazy loading
} else {
$model = $viaQuery->one(); $where = $this->where;
$this->primaryModel->populateRelation($viaName, $model);
$viaModels = $model === null ? [] : [$model]; if ($this->via instanceof self) {
} // via pivot table
$this->filterByModels($viaModels); $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 { } else {
$this->filterByModels([$this->primaryModel]); $model = $viaQuery->one();
$this->primaryModel->populateRelation($viaName, $model);
$viaModels = $model === null ? [] : [$model];
} }
$command = parent::createCommand($db); $this->filterByModels($viaModels);
$this->where = $where; } else {
return $command; $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;
} }
} }

7
tests/unit/data/ar/Order.php

@ -58,6 +58,13 @@ class Order extends ActiveRecord
->where(['category_id' => 1]); ->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) public function beforeSave($insert)
{ {
if (parent::beforeSave($insert)) { if (parent::beforeSave($insert)) {

34
tests/unit/framework/db/ActiveRecordTest.php

@ -290,5 +290,39 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertTrue($orders[0]->isRelationPopulated('customer')); $this->assertTrue($orders[0]->isRelationPopulated('customer'));
$this->assertTrue($orders[1]->isRelationPopulated('customer')); $this->assertTrue($orders[1]->isRelationPopulated('customer'));
$this->assertTrue($orders[2]->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));
} }
} }

Loading…
Cancel
Save