Yii2-多表关联的用法示例

本文通过具体的业务场景,详细展示了Yii2框架中如何处理多表关联,包括简单关联、跨中间表关联、级联关联以及joinWith方法的使用。通过示例代码和查询SQL,解释了各种关联查询的差异和效果,帮助理解Yii2的ORM关联查询机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我会构造一个业务场景,主要是测试我比较存疑的各种表关联写法,而非再次介绍基础用法。

构造场景

订单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交互拉回所有依赖数据,而是用于缩小主体数据的规模,这也是为什么后续拉取依赖的时候,需要将依赖表的过滤条件再次套用的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值