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. 47
      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);

47
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,9 +84,23 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
*/ */
public function createCommand($db = null) public function createCommand($db = null)
{ {
if ($this->primaryModel !== null) { if ($this->primaryModel === null) {
// eager loading
if (!empty($this->on)) {
$where = $this->where; $where = $this->where;
$this->andWhere($this->on);
$command = parent::createCommand($db);
$this->where = $where;
return $command;
} else {
return parent::createCommand($db);
}
}
// lazy loading // lazy loading
$where = $this->where;
if ($this->via instanceof self) { if ($this->via instanceof self) {
// via pivot table // via pivot table
$viaModels = $this->via->findPivotRows([$this->primaryModel]); $viaModels = $this->via->findPivotRows([$this->primaryModel]);
@ -85,10 +121,15 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
} else { } else {
$this->filterByModels([$this->primaryModel]); $this->filterByModels([$this->primaryModel]);
} }
if (!empty($this->on)) {
$this->andWhere($this->on);
}
$command = parent::createCommand($db); $command = parent::createCommand($db);
$this->where = $where; $this->where = $where;
return $command; return $command;
} }
return parent::createCommand($db);
}
} }

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