Commit c4c328dc by Qiang Xue

Fixes #1791: support ON condition for relational query.

parent a70808f8
......@@ -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
--------------------------
......
......@@ -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);
......
......@@ -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;
}
}
......@@ -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)) {
......
......@@ -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));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment