我会构造一个业务场景,主要是测试我比较存疑的各种表关联写法,而非再次介绍基础用法。
构造场景
订单ar_order
order_id | 订单id(主键) |
user_id | 用户id |
用户ar_user
user_id | 用户id(主键) |
user_name | 用户名 |
订单商品清单ar_order_goods
id | 自增id(主键) |
order_id | 所属订单id |
goods_id | 所买商品id |
商品ar_goods
goods_id | 商品id(主键) |
goods_name | 商品名称 |
商品库存ar_stock
stock_id | 库存id(主键) |
goods_id | 商品id(唯一键) |
stock_count | 库存量 |
表关系如下图所示:
我们接下来的测试,均以"订单"为主体,通过AR的ORM关联来查询出依赖的数据。
环境准备
除了建表,还需要用gii生成所有的AR类,另外日志至少需要开启db相关的category才能在日志里看见执行的SQL是什么。
1
2
3
4
5
6
7
8
9
10
|
'log'
=> [
'traceLevel'
=> YII_DEBUG ? 3 : 0,
'targets'
=> [
[
'class'
=>
'yii\log\FileTarget'
,
'levels'
=> [
'info'
,
'error'
,
'warning'
,
'trace'
],
'categories'
=> [
'yii\db\*'
],
],
],
],
|
简单关联
订单与用户 1:1
数据:
ar_order:
ar_user:
给ArOrder添加关联:
1
2
3
|
public
function
getUser() {
return
$this
->hasOne(ArUser::className(), [
'user_id'
=>
'user_id'
]);
}
|
测试lazyload:
1
2
3
4
5
6
7
8
9
10
11
|
public
function
actionHasOne()
{
// 查订单
$orders
= ArOrder::find()->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的用户
$user
=
$order
->user;
// 打印用户名
echo
$user
->user_name . PHP_EOL;
}
}
|
lazyload sql:
1
2
3
4
5
|
SELECT * FROM ar_order
SELECT * FROM `ar_user` WHERE `user_id`=1
SELECT * FROM `ar_user` WHERE `user_id`=2
|
测试eagerload:
1
2
3
4
5
6
7
8
9
10
11
|
public
function
actionHasOne()
{
// 查订单
$orders
= ArOrder::find()->with(
'user'
)->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的用户
$user
=
$order
->user;
// 打印用户名,输出:owen
echo
$user
->user_name . PHP_EOL;
}
}
|
eagerload sql:
1
2
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_user` WHERE `user_id` IN (1, 2)
|
订单与商品清单 1:n
数据:
ar_order_goods:
给ArOrder添加关联:
1
2
3
|
public
function
getOrderGoods() {
return
$this
->hasMany(ArOrderGoods::className(), [
'order_id'
=>
'order_id'
]);
}
|
lazyload测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
function
actionHasMany()
{
// 查订单
$orders
= ArOrder::find()->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的商品清单
$orderGoodsArr
=
$order
->orderGoods;
// 打印每个商品ID
foreach
(
$orderGoodsArr
as
$orderGoods
) {
echo
$orderGoods
->goods_id . PHP_EOL;
}
}
}
|
lazyload sql:
1
2
3
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id`=1
SELECT * FROM `ar_order_goods` WHERE `order_id`=2
|
eagerload测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
function
actionHasMany()
{
// 查订单
$orders
= ArOrder::find()->with(
'orderGoods'
)->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的商品清单
$orderGoodsArr
=
$order
->orderGoods;
// 打印每个商品ID,输出:1,2
foreach
(
$orderGoodsArr
as
$orderGoods
) {
echo
$orderGoods
->goods_id . PHP_EOL;
}
}
}
|
eagerload sql:
1
2
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id` IN (1, 2)
|
跨中间表关联
订单 与 商品表 跨 商品清单表 1:n关联
数据:
ar_goods:
给ArOrder添加关联:
1
2
3
4
5
6
7
8
|
public
function
getOrderGoods() {
return
$this
->hasMany(ArOrderGoods::className(), [
'order_id'
=>
'order_id'
]);
}
public
function
getGoods() {
return
$this
->hasMany(ArGoods::className(), [
'goods_id'
=>
'goods_id'
])->
via(
'orderGoods'
);
}
|
注:getGoods中的第一个goods_id是指getOrderGoods关联的ArOrderGoods中的goods_id,第二个goods_id是指ArGoods中的goods_id。
lazyLoad测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
function
actionVia()
{
// 查订单
$orders
= ArOrder::find()->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的商品(跨中间表orderGoods)
$goodsArr
=
$order
->goods;
// 中间表$order->orderGoods的数据在此也被拉回来
echo
count
(
$order
->orderGoods) . PHP_EOL;
// 打印每个商品的名称
foreach
(
$goodsArr
as
$goods
) {
echo
$goods
->goods_name .
' '
. PHP_EOL;
}
}
}
|
lazyload sql:
1
2
3
4
5
6
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id`=1
SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
SELECT * FROM `ar_order_goods` WHERE `order_id`=2
SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
|
eagerload测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
function
actionVia()
{
// 查订单
$orders
= ArOrder::find()->with(
"goods"
)->all();
foreach
(
$orders
as
$order
) {
// 查订单关联的商品(跨中间表orderGoods)
$goodsArr
=
$order
->goods;
// 中间表$order->orderGoods的数据在此也被拉回来
echo
count
(
$order
->orderGoods) . PHP_EOL;
// 打印每个商品的名称
foreach
(
$goodsArr
as
$goods
) {
echo
$goods
->goods_name .
' '
. PHP_EOL;
}
}
}
|
eagerload sql:
1
2
3
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id` IN (1, 2)
SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
|
发现with仅指定goods关联,则中间关联orderGoods的查询也被eager处理了。
简单关联之级联
和跨中间表关联实现的功能一致,但是不通过via实现,而是通过定义若干级联的1:1或1:n关联来加载数据。
上述中间表关联中,ArOrder是主体,orderGoods和goods都被注入在ArOrder对象身上,这样的优点是eagerload可以优化整个查询流程,减少db交互,同时冗余表达的goods对象少(只需要2个goods对象,由2个order共享,下面代码可以测试):
1
|
$orders
[0]->goods[0] ===
$orders
[1]->goods[0]
|
另一种表达这种关系的方式是:arOrder->orderGoods->goods这种间接访问的方式,这样仅需要维护arOrder和orderGoods间的1:n关系以及orderGoods和Goods间的1:1关系既可,优点是访问方式更能体现表关联的间接性,但是缺点就是eagerload无法完整优化整个流程,同时goods对象冗余多。
订单 ,商品表 ,商品清单表 级联
ArOrderGoods添加关联:
1
2
3
|
public
function
getGoods() {
return
$this
->hasOne(ArGoods::className(), [
'goods_id'
=>
'goods_id'
]);
}
|
lazyload测试:
1
2
3
4
5
6
7
8
9
10
11
12
|
public
function
actionNoVia()
{
$orders
= ArOrder::find()->all();
foreach
(
$orders
as
$order
) {
$orderGoodsArr
=
$order
->orderGoods;
foreach
(
$orderGoodsArr
as
$orderGoods
) {
$goods
=
$orderGoods
->goods;
echo
$goods
->goods_name . PHP_EOL;
}
}
}
|
lazyload sql:
1
2
3
4
5
6
7
8
9
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id`=1
SELECT * FROM `ar_goods` WHERE `goods_id`=1
SELECT * FROM `ar_goods` WHERE `goods_id`=2
SELECT * FROM `ar_order_goods` WHERE `order_id`=2
SELECT * FROM `ar_goods` WHERE `goods_id`=1
SELECT * FROM `ar_goods` WHERE `goods_id`=2
|
eagerload测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
function
actionNoVia()
{
// 第一级关系eagerload
$orders
= ArOrder::find()->with(
'orderGoods'
)->all();
foreach
(
$orders
as
$order
) {
// 第二级关系eagerload
$orderGoodsArr
=
$order
->getOrderGoods()->with(
'goods'
)->all();
foreach
(
$orderGoodsArr
as
$orderGoods
) {
$goods
=
$orderGoods
->goods;
echo
$goods
->goods_name . PHP_EOL;
}
}
}
|
eagerload sql:
1
2
3
4
5
6
7
8
|
SELECT * FROM `ar_order`
SELECT * FROM `ar_order_goods` WHERE `order_id` IN (1, 2)
SELECT * FROM `ar_order_goods` WHERE `order_id`=1
SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
SELECT * FROM `ar_order_goods` WHERE `order_id`=2
SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
|
可见,级联方式的交互总是比中间表方式要多,内存占用也要多,虽然经过eagerload优化可以减少几次交互。
joinWith 多表关联
Yii2支持数据库的join语法,不过在编程的时候不是a表join b表这样的表达方式,而是a表通过哪个关联进行join,这个关联就是我们之前定义的hasOne和hasMany,它们是不需要变动的。
不过Yii2的JOIN并不是你想的那样:"一句SQL查回所有的关联数据,填充到关联关系里",这是非常特殊的地方,文档里这样提到:
joinWith() 和 with() 的差别在于前者是联合查询,即通过把查询条件应用于主表和关联表来获取主表记录,而后者是关联查询,即只是针对主表查询条件获取主表记录。
因为这个差别,你可以应用JOIN SQL语句特有的查询条件。比如你可以通过限定关联表的条件来过滤主表记录,如上述例子所示。你还可以通过关联表列值来对主表记录进行排序。
说白了,joinWith虽然是使用数据库的join语法实现的多表联查,但是它不会一次性的将依赖表的数据保存起来,与with相比,仅仅是额外提供了一个根据依赖表的数据过滤主表数据的机会,依赖表的数据依旧会通过再次交互的方式进行查询,是不是既失望又好奇呢?
订单,商品清单,商品 JOIN
测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
function
actionJoin() {
$orders
= ArOrder::find()->innerJoinWith([
'user'
=>
function
(
$query
) {
$query
->onCondition([
'!='
,
'user_name'
,
'john'
]);
},
'goods'
=>
function
(
$query
) {
$query
->onCondition([
'and'
,
[
'!='
,
'goods_name'
,
'雪碧'
],
]);
}
])->all();
foreach
(
$orders
as
$order
) {
$goodsArr
=
$order
->goods;
foreach
(
$goodsArr
as
$goods
) {
echo
$goods
->goods_name . PHP_EOL;
}
}
}
|
sql:
1
2
3
4
5
6
7
|
SELECT `ar_order`.* FROM `ar_order` INNER JOIN `ar_user` ON (`ar_order`.`user_id` = `ar_user`.`user_id`) AND (`user_name` !=
'john'
) INNER JOIN `ar_order_goods` ON `ar_order`.`order_id` = `ar_order_goods`.`order_id` INNER JOIN `ar_goods` ON (`ar_order_goods`.`goods_id` = `ar_goods`.`goods_id`) AND ((`goods_name` !=
'雪碧'
))
SELECT * FROM `ar_user` WHERE (`user_id`=1) AND (`user_name` !=
'john'
)
SELECT * FROM `ar_order_goods` WHERE `order_id`=1
SELECT * FROM `ar_goods` WHERE (`goods_id` IN (1, 2)) AND ((`goods_name` !=
'雪碧'
))
|
分析:
你会发现,joinWith的确不是我们所想的一次SQL交互拉回所有依赖数据,而是用于缩小主体数据的规模,这也是为什么后续拉取依赖的时候,需要将依赖表的过滤条件再次套用的原因。