Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PSDI Army
yii2
Commits
4f44bb24
Commit
4f44bb24
authored
Dec 24, 2013
by
Qiang Xue
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fixes #1581: Added `ActiveQuery::joinWith()` to support joining with relations
parent
2402d2d0
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
212 additions
and
24 deletions
+212
-24
active-record.md
docs/guide/active-record.md
+24
-0
CHANGELOG.md
framework/CHANGELOG.md
+1
-0
ActiveQuery.php
framework/yii/db/ActiveQuery.php
+112
-4
ActiveRelationTrait.php
framework/yii/db/ActiveRelationTrait.php
+20
-20
Category.php
tests/unit/data/ar/Category.php
+27
-0
Item.php
tests/unit/data/ar/Item.php
+5
-0
ActiveRecordTest.php
tests/unit/framework/db/ActiveRecordTest.php
+23
-0
No files found.
docs/guide/active-record.md
View file @
4f44bb24
...
@@ -386,6 +386,30 @@ $customers = Customer::find()->limit(100)->with([
...
@@ -386,6 +386,30 @@ $customers = Customer::find()->limit(100)->with([
```
```
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,
```
php
// 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.name'
);
}
])
->
all
();
```
Note that
[
[ActiveQuery::joinWith()
]
] differs from
[
[ActiveQuery::with()
]
] in that the former will build up
and execute a JOIN SQL statement. 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.
Working with Relationships
Working with Relationships
--------------------------
--------------------------
...
...
framework/CHANGELOG.md
View file @
4f44bb24
...
@@ -27,6 +27,7 @@ Yii Framework 2 Change Log
...
@@ -27,6 +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 #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 #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 #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 #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight)
-
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
`favicon.ico`
and
`robots.txt`
to defauly application templates (samdark)
-
Enh: Added
`Widget::autoIdPrefix`
to support prefixing automatically generated widget IDs (qiangxue)
-
Enh: Added
`Widget::autoIdPrefix`
to support prefixing automatically generated widget IDs (qiangxue)
...
...
framework/yii/db/ActiveQuery.php
View file @
4f44bb24
...
@@ -68,6 +68,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -68,6 +68,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$rows
=
$command
->
queryAll
();
$rows
=
$command
->
queryAll
();
if
(
!
empty
(
$rows
))
{
if
(
!
empty
(
$rows
))
{
$models
=
$this
->
createModels
(
$rows
);
$models
=
$this
->
createModels
(
$rows
);
if
(
!
empty
(
$this
->
join
)
&&
$this
->
indexBy
===
null
)
{
$models
=
$this
->
removeDuplicatedModels
(
$models
);
}
if
(
!
empty
(
$this
->
with
))
{
if
(
!
empty
(
$this
->
with
))
{
$this
->
findWith
(
$this
->
with
,
$models
);
$this
->
findWith
(
$this
->
with
,
$models
);
}
}
...
@@ -78,6 +81,47 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -78,6 +81,47 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
/**
/**
* Removes duplicated models by checking their primary key values.
* This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
* @param array $models the models to be checked
* @return array the distinctive models
*/
private
function
removeDuplicatedModels
(
$models
)
{
$hash
=
[];
/** @var ActiveRecord $class */
$class
=
$this
->
modelClass
;
$pks
=
$class
::
primaryKey
();
if
(
count
(
$pks
)
>
1
)
{
foreach
(
$models
as
$i
=>
$model
)
{
$key
=
[];
foreach
(
$pks
as
$pk
)
{
$key
[]
=
$model
[
$pk
];
}
$key
=
serialize
(
$key
);
if
(
isset
(
$hash
[
$key
]))
{
unset
(
$models
[
$i
]);
}
else
{
$hash
[
$key
]
=
true
;
}
}
}
else
{
$pk
=
reset
(
$pks
);
foreach
(
$models
as
$i
=>
$model
)
{
$key
=
$model
[
$pk
];
if
(
isset
(
$hash
[
$key
]))
{
unset
(
$models
[
$i
]);
}
else
{
$hash
[
$key
]
=
true
;
}
}
}
return
array_values
(
$models
);
}
/**
* Executes query and returns a single row of result.
* Executes query and returns a single row of result.
* @param Connection $db the DB connection used to create the DB command.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* If null, the DB connection returned by [[modelClass]] will be used.
...
@@ -144,6 +188,42 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -144,6 +188,42 @@ class ActiveQuery extends Query implements ActiveQueryInterface
return
$db
->
createCommand
(
$sql
,
$params
);
return
$db
->
createCommand
(
$sql
,
$params
);
}
}
/**
* Joins with the specified relations.
*
* This method allows you to reuse existing relation definitions to perform JOIN queries.
* Based on the definition of the specified relation(s), the method will append one or multiple
* JOIN statements to the current query.
*
* If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations,
* which is equivalent to calling [[with()]] using the specified relations.
*
* 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.
*
* @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
* can be used to modify the relation queries on-the-fly. If a relation query does not need modification,
* you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]).
* For example,
*
* ```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' => function ($query) {
* $query->orderBy('tbl_item.name');
* }
* ])->all();
* ```
*
* @param bool $eagerLoading
* @param string $joinType
* @return $this
*/
public
function
joinWith
(
$with
,
$eagerLoading
=
true
,
$joinType
=
'INNER JOIN'
)
public
function
joinWith
(
$with
,
$eagerLoading
=
true
,
$joinType
=
'INNER JOIN'
)
{
{
$with
=
(
array
)
$with
;
$with
=
(
array
)
$with
;
...
@@ -167,9 +247,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -167,9 +247,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
/**
/**
* @param ActiveRecord $model
* Modifies the current query by adding join fragments based on the given relations.
* @param array $with
* @param ActiveRecord $model the primary model
* @param string|array $joinType
* @param array $with the relations to be joined
* @param string|array $joinType the join type
*/
*/
private
function
joinWithRelations
(
$model
,
$with
,
$joinType
)
private
function
joinWithRelations
(
$model
,
$with
,
$joinType
)
{
{
...
@@ -211,6 +292,12 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -211,6 +292,12 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
}
}
/**
* Returns the join type based on the given join type parameter and the relation name.
* @param string|array $joinType the given join type(s)
* @param string $name relation name
* @return string the real join type
*/
private
function
getJoinType
(
$joinType
,
$name
)
private
function
getJoinType
(
$joinType
,
$name
)
{
{
if
(
is_array
(
$joinType
)
&&
isset
(
$joinType
[
$name
]))
{
if
(
is_array
(
$joinType
)
&&
isset
(
$joinType
[
$name
]))
{
...
@@ -221,8 +308,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -221,8 +308,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
/**
/**
* Returns the table name used by the specified active query.
* @param ActiveQuery $query
* @param ActiveQuery $query
* @return string
* @return string
the table name
*/
*/
private
function
getQueryTableName
(
$query
)
private
function
getQueryTableName
(
$query
)
{
{
...
@@ -236,14 +324,32 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -236,14 +324,32 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
/**
/**
* Joins a parent query with a child query.
* The current query object will be modified accordingly.
* @param ActiveQuery $parent
* @param ActiveQuery $parent
* @param ActiveRelation $child
* @param ActiveRelation $child
* @param string $joinType
* @param string $joinType
*/
*/
private
function
joinWithRelation
(
$parent
,
$child
,
$joinType
)
private
function
joinWithRelation
(
$parent
,
$child
,
$joinType
)
{
{
$via
=
$child
->
via
;
$child
->
via
=
null
;
if
(
$via
instanceof
ActiveRelation
)
{
// via table
$this
->
joinWithRelation
(
$parent
,
$via
,
$joinType
);
$this
->
joinWithRelation
(
$via
,
$child
,
$joinType
);
return
;
}
elseif
(
is_array
(
$via
))
{
// via relation
$this
->
joinWithRelation
(
$parent
,
$via
[
1
],
$joinType
);
$this
->
joinWithRelation
(
$via
[
1
],
$child
,
$joinType
);
return
;
}
$parentTable
=
$this
->
getQueryTableName
(
$parent
);
$parentTable
=
$this
->
getQueryTableName
(
$parent
);
$childTable
=
$this
->
getQueryTableName
(
$child
);
$childTable
=
$this
->
getQueryTableName
(
$child
);
if
(
!
empty
(
$child
->
link
))
{
if
(
!
empty
(
$child
->
link
))
{
$on
=
[];
$on
=
[];
foreach
(
$child
->
link
as
$childColumn
=>
$parentColumn
)
{
foreach
(
$child
->
link
as
$childColumn
=>
$parentColumn
)
{
...
@@ -254,6 +360,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface
...
@@ -254,6 +360,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$on
=
''
;
$on
=
''
;
}
}
$this
->
join
(
$joinType
,
$childTable
,
$on
);
$this
->
join
(
$joinType
,
$childTable
,
$on
);
if
(
!
empty
(
$child
->
where
))
{
if
(
!
empty
(
$child
->
where
))
{
$this
->
andWhere
(
$child
->
where
);
$this
->
andWhere
(
$child
->
where
);
}
}
...
...
framework/yii/db/ActiveRelationTrait.php
View file @
4f44bb24
...
@@ -189,26 +189,6 @@ trait ActiveRelationTrait
...
@@ -189,26 +189,6 @@ trait ActiveRelationTrait
}
}
/**
/**
* @param ActiveRecord|array $model
* @param array $attributes
* @return string
*/
private
function
getModelKey
(
$model
,
$attributes
)
{
if
(
count
(
$attributes
)
>
1
)
{
$key
=
[];
foreach
(
$attributes
as
$attribute
)
{
$key
[]
=
$model
[
$attribute
];
}
return
serialize
(
$key
);
}
else
{
$attribute
=
reset
(
$attributes
);
$key
=
$model
[
$attribute
];
return
is_scalar
(
$key
)
?
$key
:
serialize
(
$key
);
}
}
/**
* @param array $models
* @param array $models
*/
*/
private
function
filterByModels
(
$models
)
private
function
filterByModels
(
$models
)
...
@@ -237,6 +217,26 @@ trait ActiveRelationTrait
...
@@ -237,6 +217,26 @@ trait ActiveRelationTrait
}
}
/**
/**
* @param ActiveRecord|array $model
* @param array $attributes
* @return string
*/
private
function
getModelKey
(
$model
,
$attributes
)
{
if
(
count
(
$attributes
)
>
1
)
{
$key
=
[];
foreach
(
$attributes
as
$attribute
)
{
$key
[]
=
$model
[
$attribute
];
}
return
serialize
(
$key
);
}
else
{
$attribute
=
reset
(
$attributes
);
$key
=
$model
[
$attribute
];
return
is_scalar
(
$key
)
?
$key
:
serialize
(
$key
);
}
}
/**
* @param array $primaryModels either array of AR instances or arrays
* @param array $primaryModels either array of AR instances or arrays
* @return array
* @return array
*/
*/
...
...
tests/unit/data/ar/Category.php
0 → 100644
View file @
4f44bb24
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace
yiiunit\data\ar
;
/**
* Class Category.
*
* @property integer $id
* @property string $name
*/
class
Category
extends
ActiveRecord
{
public
static
function
tableName
()
{
return
'tbl_category'
;
}
public
function
getItems
()
{
return
$this
->
hasMany
(
Item
::
className
(),
[
'category_id'
=>
'id'
]);
}
}
tests/unit/data/ar/Item.php
View file @
4f44bb24
...
@@ -15,4 +15,9 @@ class Item extends ActiveRecord
...
@@ -15,4 +15,9 @@ class Item extends ActiveRecord
{
{
return
'tbl_item'
;
return
'tbl_item'
;
}
}
public
function
getCategory
()
{
return
$this
->
hasOne
(
Category
::
className
(),
[
'id'
=>
'category_id'
]);
}
}
}
tests/unit/framework/db/ActiveRecordTest.php
View file @
4f44bb24
...
@@ -243,5 +243,28 @@ class ActiveRecordTest extends DatabaseTestCase
...
@@ -243,5 +243,28 @@ class ActiveRecordTest extends DatabaseTestCase
$this
->
assertEquals
(
3
,
$orders
[
1
]
->
id
);
$this
->
assertEquals
(
3
,
$orders
[
1
]
->
id
);
$this
->
assertFalse
(
$orders
[
0
]
->
isRelationPopulated
(
'customer'
));
$this
->
assertFalse
(
$orders
[
0
]
->
isRelationPopulated
(
'customer'
));
$this
->
assertFalse
(
$orders
[
1
]
->
isRelationPopulated
(
'customer'
));
$this
->
assertFalse
(
$orders
[
1
]
->
isRelationPopulated
(
'customer'
));
// join with via-relation
$orders
=
Order
::
find
()
->
joinWith
(
'books'
)
->
orderBy
(
'tbl_order.id'
)
->
all
();
$this
->
assertEquals
(
2
,
count
(
$orders
));
$this
->
assertEquals
(
1
,
$orders
[
0
]
->
id
);
$this
->
assertEquals
(
3
,
$orders
[
1
]
->
id
);
$this
->
assertTrue
(
$orders
[
0
]
->
isRelationPopulated
(
'books'
));
$this
->
assertTrue
(
$orders
[
1
]
->
isRelationPopulated
(
'books'
));
$this
->
assertEquals
(
2
,
count
(
$orders
[
0
]
->
books
));
$this
->
assertEquals
(
1
,
count
(
$orders
[
1
]
->
books
));
// join with sub-relation
$orders
=
Order
::
find
()
->
joinWith
([
'items.category'
=>
function
(
$q
)
{
$q
->
where
(
'tbl_category.id = 2'
);
},
])
->
orderBy
(
'tbl_order.id'
)
->
all
();
$this
->
assertEquals
(
1
,
count
(
$orders
));
$this
->
assertTrue
(
$orders
[
0
]
->
isRelationPopulated
(
'items'
));
$this
->
assertEquals
(
2
,
$orders
[
0
]
->
id
);
$this
->
assertEquals
(
3
,
count
(
$orders
[
0
]
->
items
));
$this
->
assertTrue
(
$orders
[
0
]
->
items
[
0
]
->
isRelationPopulated
(
'category'
));
$this
->
assertEquals
(
2
,
$orders
[
0
]
->
items
[
0
]
->
category
->
id
);
}
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment