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' => 'yii\log\FileTarget',
                    'levels' => ['info', 'error', 'warning', 'trace'],
                    'categories' => ['yii\db\*'],
                ],
            ],
        ],

簡單關聯

訂單與用戶 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給我們帶來的抽象性和編程效率。

 

感興趣請點擊關注我,歡迎討論


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM