From 03451912455dcf43229b92f8afa463448ec43fa0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 24 Dec 2013 21:27:13 -0500 Subject: [PATCH] Added ActiveQuery::innerJoinWith(). --- docs/guide/active-record.md | 61 +++++++++++++++------------- framework/CHANGELOG.md | 2 +- framework/yii/db/ActiveQuery.php | 24 ++++++++--- tests/unit/framework/db/ActiveRecordTest.php | 18 ++++++-- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 0996331..a414bce 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -391,35 +391,26 @@ Joining with Relations When working with relational databases, a common task is to join multiple tables and apply various query conditions and parameters to the JOIN SQL statement. Instead of calling [[ActiveQuery::join()]] -explicitly to build up the JOIN query, you may reuse the existing relation definitions and call [[ActiveQuery::joinWith()]] -to achieve the same goal. For example, +explicitly to build up the JOIN query, you may reuse the existing relation definitions and call +[[ActiveQuery::joinWith()]] to achieve this goal. For example, ```php +// find all orders and sort the orders by the customer id and the order id. also eager loading "customer" +$orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id, tbl_order.id')->all(); // find all orders that contain books, and eager loading "books" -$orders = Order::find()->joinWith('books')->all(); -// find all orders that contain books, and sort the orders by the book names. -$orders = Order::find()->joinWith([ - 'books' => function ($query) { - $query->orderBy('tbl_item.id'); - } -])->all(); +$orders = Order::find()->innerJoinWith('books')->all(); ``` -Note that [[ActiveQuery::joinWith()]] differs from [[ActiveQuery::with()]] in that the former will build up -and execute a JOIN SQL statement for the primary model class. For example, `Order::find()->joinWith('books')->all()` -returns all orders that contain books, while `Order::find()->with('books')->all()` returns all orders -regardless they contain books or not. - -Because `joinWith()` will cause generating a JOIN SQL statement, you are responsible to disambiguate column -names. For example, we use `tbl_item.id` to disambiguate the `id` column reference because both of the order table -and the item table contain a column named `id`. +In the above, the method [[ActiveQuery::innerJoinWith()|innerJoinWith()]] is a shortcut to [[ActiveQuery::joinWith()|joinWith()]] +with the join type set as `INNER JOIN`. -You may join with one or multiple relations. You may also join with sub-relations. For example, +You may join with one or multiple relations; you may apply query conditions to the relations on-the-fly; +and you may also join with sub-relations. For example, ```php // join with multiple relations // find out the orders that contain books and are placed by customers who registered within the past 24 hours -$orders = Order::find()->joinWith([ +$orders = Order::find()->innerJoinWith([ 'books', 'customer' => function ($query) { $query->where('tbl_customer.create_time > ' . (time() - 24 * 3600)); @@ -429,23 +420,37 @@ $orders = Order::find()->joinWith([ $orders = Order::find()->joinWith('books.author')->all(); ``` +Behind the scene, Yii will first execute a JOIN SQL statement to bring back the primary models +satisfying the conditions applied to the JOIN SQL. It will then execute a query for each relation +and populate the corresponding related records. + +The difference between [[ActiveQuery::joinWith()|joinWith()]] and [[ActiveQuery::with()|with()]] is that +the former joins the tables for the primary model class and the related model classes to retrieve +the primary models, while the latter just queries against the table for the primary model class to +retrieve the primary models. + +Because of this difference, you may apply query conditions that are only available to a JOIN SQL statement. +For example, you may filter the primary models by the conditions on the related models, like the example +above. You may also sort the primary models using columns from the related tables. + +When using [[ActiveQuery::joinWith()|joinWith()]], you are responsible to disambiguate column names. +In the above examples, we use `tbl_item.id` and `tbl_order.id` to disambiguate the `id` column references +because both of the order table and the item table contain a column named `id`. + By default, when you join with a relation, the relation will also be eagerly loaded. You may change this behavior by passing the `$eagerLoading` parameter which specifies whether to eager load the specified relations. -Also, when the relations are joined with the primary table, the default join type is `INNER JOIN`. You may change -to use other type of joins, such as `LEFT JOIN`. +And also by default, [[ActiveQuery::joinWith()|joinWith()]] uses `LEFT JOIN` to join the related tables. +You may pass it with the `$joinType` parameter to customize the join type. As a shortcut to the `INNER JOIN` type, +you may use [[ActiveQuery::innerJoinWith()|innerJoinWith()]]. Below are some more examples, ```php // find all orders that contain books, but do not eager loading "books". -$orders = Order::find()->joinWith('books', false)->all(); -// find all orders and sort them by the customer IDs. Do not eager loading "customer". -$orders = Order::find()->joinWith([ - 'customer' => function ($query) { - $query->orderBy('tbl_customer.id'); - }, -], false, 'LEFT JOIN')->all(); +$orders = Order::find()->innerJoinWith('books', false)->all(); +// equivalent to the above +$orders = Order::find()->joinWith('books', false, 'INNER JOIN')->all(); ``` diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 562277e..d94011f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,7 +27,7 @@ Yii Framework 2 Change Log - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) -- Enh #1581: Added `ActiveQuery::joinWith()` to support joining with relations (qiangxue) +- Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue) - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index a71b1cf..26b0c6e 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -200,8 +200,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface * * Note that because a JOIN query will be performed, you are responsible to disambiguate column names. * - * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement. - * When `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. + * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement + * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. * * @param array $with the relations to be joined. Each array element represents a single relation. * The array keys are relation names, and the array values are the corresponding anonymous functions that @@ -211,8 +211,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface * * ```php * // find all orders that contain books, and eager loading "books" - * Order::find()->joinWith('books')->all(); - * // find all orders that contain books, and sort the orders by the book names. + * Order::find()->joinWith('books', true, 'INNER JOIN')->all(); + * // find all orders, eager loading "books", and sort the orders and books by the book names. * Order::find()->joinWith([ * 'books' => function ($query) { * $query->orderBy('tbl_item.name'); @@ -228,7 +228,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface * in the format of `relationName => joinType` to specify different join types for different relations. * @return static the query object itself */ - public function joinWith($with, $eagerLoading = true, $joinType = 'INNER JOIN') + public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') { $with = (array)$with; $this->joinWithRelations(new $this->modelClass, $with, $joinType); @@ -251,6 +251,20 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** + * Inner joins with the specified relations. + * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN". + * Please refer to [[joinWith()]] for detailed usage of this method. + * @param array $with the relations to be joined with + * @param boolean|array $eagerLoading whether to eager loading the relations + * @return static the query object itself + * @see joinWith() + */ + public function innerJoinWith($with, $eagerLoading = true) + { + return $this->joinWith($with, $eagerLoading, 'INNER JOIN'); + } + + /** * Modifies the current query by adding join fragments based on the given relations. * @param ActiveRecord $model the primary model * @param array $with the relations to be joined diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 276547e..40050e5 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -220,8 +220,18 @@ class ActiveRecordTest extends DatabaseTestCase public function testJoinWith() { + // left join and eager loading + $orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + // inner join filtering and eager loading - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'customer' => function ($query) { $query->where('tbl_customer.id=2'); }, @@ -233,7 +243,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue($orders[1]->isRelationPopulated('customer')); // inner join filtering without eager loading - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'customer' => function ($query) { $query->where('tbl_customer.id=2'); }, @@ -245,7 +255,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertFalse($orders[1]->isRelationPopulated('customer')); // join with via-relation - $orders = Order::find()->joinWith('books')->orderBy('tbl_order.id')->all(); + $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all(); $this->assertEquals(2, count($orders)); $this->assertEquals(1, $orders[0]->id); $this->assertEquals(3, $orders[1]->id); @@ -255,7 +265,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(1, count($orders[1]->books)); // join with sub-relation - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'items.category' => function ($q) { $q->where('tbl_category.id = 2'); },