• Yii2-多表关联的用法示例


    本篇博客是基于《活动记录(Active Record)》中对于AR表关联用法的介绍。

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

    构造场景

    订单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 库存量

    表关系如下图所示:

    machi-2016-12-12-14-09-08

    我们接下来的测试,均以"订单"为主体,通过AR的ORM关联来查询出依赖的数据。

    环境准备

    除了建表,还需要用gii生成所有的AR类,另外日志至少需要开启db相关的category才能在日志里看见执行的SQL是什么。

            'log' => [
                'traceLevel' => YII_DEBUG ? 3 : 0,
                'targets' => [
                    [
                        'class' => 'yiilogFileTarget',
                        'levels' => ['info', 'error', 'warning', 'trace'],
                        'categories' => ['yiidb*'],
                    ],
                ],
            ],

    简单关联

    订单与用户 1:1

    数据:

    ar_order: ar_order

    ar_user: ar_user

    给ArOrder添加关联:

        public function getUser() {
            return $this->hasOne(ArUser::className(), ['user_id' => 'user_id']);
        }

    测试lazyload:

        public function actionHasOne()
        {
            // 查订单
            $orders = ArOrder::find()->all();
            foreach ($orders as $order) {
                // 查订单关联的用户
                $user = $order->user;
                // 打印用户名
                echo $user->user_name . PHP_EOL;
            }
        }
    lazyload sql:
    SELECT * FROM ar_order
    
    SELECT * FROM `ar_user` WHERE `user_id`=1
    
    SELECT * FROM `ar_user` WHERE `user_id`=2

    测试eagerload:

        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:

    SELECT * FROM `ar_order`
    SELECT * FROM `ar_user` WHERE `user_id` IN (1, 2)

    订单与商品清单 1:n

    数据:

    ar_order_goods:ar_order_goods

    给ArOrder添加关联:

        public function getOrderGoods() {
            return $this->hasMany(ArOrderGoods::className(), ['order_id' => 'order_id']);
        }

    lazyload测试:

        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:

    SELECT * FROM `ar_order`
    SELECT * FROM `ar_order_goods` WHERE `order_id`=1
    SELECT * FROM `ar_order_goods` WHERE `order_id`=2

    eagerload测试:

        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:

    SELECT * FROM `ar_order`
    SELECT * FROM `ar_order_goods` WHERE `order_id` IN (1, 2)

    跨中间表关联

    订单 与 商品表 跨 商品清单表 1:n关联

    数据:

    ar_goods:ar_goods

    给ArOrder添加关联:

        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测试:

        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:

    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测试:

        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:

    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共享,下面代码可以测试):

    $orders[0]->goods[0] === $orders[1]->goods[0]

    另一种表达这种关系的方式是:arOrder->orderGoods->goods这种间接访问的方式,这样仅需要维护arOrder和orderGoods间的1:n关系以及orderGoods和Goods间的1:1关系既可,优点是访问方式更能体现表关联的间接性,但是缺点就是eagerload无法完整优化整个流程,同时goods对象冗余多。

    订单 商品表 ,商品清单表 级联

    ArOrderGoods添加关联:

        public function getGoods() {
            return $this->hasOne(ArGoods::className(), ['goods_id' => 'goods_id']);
        }

    lazyload测试:

        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:

    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测试:

        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:

    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

    测试:

        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:

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

    通过最后的例子,我们可以明显的感受出:ORM背后的行为并不一定是我们预期的那样!

    所以,当我们使用ORM进行表关联的时候,需要认真考虑一下是不是裸写SQL的方式性能更佳,但是也别忘记ORM给我们带来的抽象性和编程效率。

    感兴趣请点击关注我,欢迎讨论

  • 相关阅读:
    CURL常用命令
    极客无极限 一行HTML5代码引发的创意大爆炸
    JS的prototype和__proto__(含es6的class)
    leetcode 44:construct-binary-tree-from-preorder-and-inorder
    leetcode 43:construct-binary-tree-from-inorder-and-postorder
    leetcode 42:binary-tree-level-order-traversal-ii
    leetcode 38:path-sum
    leetcode 37:path-sum-ii
    leetcode 33:pascals-triangle
    leetcode 32:pascals-triangle-ii
  • 原文地址:https://www.cnblogs.com/qq120848369/p/6165618.html
Copyright © 2020-2023  润新知