Skip to content

Commit

Permalink
Fixes #1791: support ON condition for relational query.
Browse files Browse the repository at this point in the history
  • Loading branch information
qiangxue committed Jan 7, 2014
1 parent a70808f commit c4c328d
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 27 deletions.
37 changes: 36 additions & 1 deletion docs/guide/active-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------
Expand Down
5 changes: 4 additions & 1 deletion framework/yii/db/ActiveQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,11 @@ private function joinWithRelation($parent, $child, $joinType)
$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);

Expand Down
91 changes: 66 additions & 25 deletions framework/yii/db/ActiveRelation.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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.
Expand Down Expand Up @@ -62,33 +84,52 @@ public function viaTable($tableName, $link, $callable = null)
*/
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;
}
}
7 changes: 7 additions & 0 deletions tests/unit/data/ar/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ public function getBooks()
->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)) {
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/framework/db/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,39 @@ public function testJoinWith()
$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));
}
}

0 comments on commit c4c328d

Please sign in to comment.