本篇博客是基于《活動(dòng)記錄(Active Record)》中對(duì)于AR表關(guān)聯(lián)用法的介紹。
我會(huì)構(gòu)造一個(gè)業(yè)務(wù)場(chǎng)景,主要是測(cè)試我比較存疑的各種表關(guān)聯(lián)寫(xiě)法,而非再次介紹基礎(chǔ)用法。
構(gòu)造場(chǎng)景 訂單ar_order order_id 訂單id(主鍵) user_id 用戶id商品庫(kù)存ar_stock
stock_id 庫(kù)存id(主鍵) goods_id 商品id(唯一鍵) stock_count 庫(kù)存量表關(guān)系如下圖所示:
			
我們接下來(lái)的測(cè)試,均以'訂單'為主體,通過(guò)AR的ORM關(guān)聯(lián)來(lái)查詢出依賴的數(shù)據(jù)。
環(huán)境準(zhǔn)備除了建表,還需要用gii生成所有的AR類,另外日志至少需要開(kāi)啟db相關(guān)的category才能在日志里看見(jiàn)執(zhí)行的SQL是什么。
'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'html' target='_blank'>class' => 'yiilogFileTarget', 'levels' => ['info', 'error', 'warning', 'trace'], 'categories' => ['yiidb*'], ], ], ],簡(jiǎn)單關(guān)聯(lián) 訂單與用戶 1:1
數(shù)據(jù):
			ar_order:
			ar_user:
給ArOrder添加關(guān)聯(lián):
public function getUser() { return $this->hasOne(ArUser::className(), ['user_id' => 'user_id']); }
測(cè)試lazyload:
    public function actionHasOne()    {        // 查訂單        $orders = ArOrder::find()->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的用戶            $user = $order->user;            // 打印用戶名            echo $user->user_name . PHP_EOL;        }    }		lazyload sql:
SELECT * FROM ar_orderSELECT * FROM `ar_user` WHERE `user_id`=1SELECT * FROM `ar_user` WHERE `user_id`=2
測(cè)試eagerload:
    public function actionHasOne()    {        // 查訂單        $orders = ArOrder::find()->with('user')->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的用戶            $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
數(shù)據(jù):
			ar_order_goods:
給ArOrder添加關(guān)聯(lián):
    public function getOrderGoods() {        return $this->hasMany(ArOrderGoods::className(), ['order_id' => 'order_id']);    }		lazyload測(cè)試:
    public function actionHasMany()    {        // 查訂單        $orders = ArOrder::find()->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的商品清單            $orderGoodsArr = $order->orderGoods;            // 打印每個(gè)商品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`=1SELECT * FROM `ar_order_goods` WHERE `order_id`=2
eagerload測(cè)試:
    public function actionHasMany()    {        // 查訂單        $orders = ArOrder::find()->with('orderGoods')->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的商品清單            $orderGoodsArr = $order->orderGoods;            // 打印每個(gè)商品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)跨中間表關(guān)聯(lián) 訂單與商品表跨商品清單表1:n關(guān)聯(lián)
數(shù)據(jù):
			ar_goods:
給ArOrder添加關(guān)聯(lián):
    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中的第一個(gè)goods_id是指getOrderGoods關(guān)聯(lián)的ArOrderGoods中的goods_id,第二個(gè)goods_id是指ArGoods中的goods_id。
lazyLoad測(cè)試:
    public function actionVia()    {        // 查訂單        $orders = ArOrder::find()->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的商品(跨中間表orderGoods)            $goodsArr = $order->goods;            // 中間表$order->orderGoods的數(shù)據(jù)在此也被拉回來(lái)            echo count($order->orderGoods) . PHP_EOL;            // 打印每個(gè)商品的名稱            foreach ($goodsArr as $goods) {                echo $goods->goods_name . ' ' . PHP_EOL;            }        }    }		lazyload sql:
SELECT * FROM `ar_order`SELECT * FROM `ar_order_goods` WHERE `order_id`=1SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)SELECT * FROM `ar_order_goods` WHERE `order_id`=2SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
eagerload測(cè)試:
    public function actionVia()    {        // 查訂單        $orders = ArOrder::find()->with('goods')->all();        foreach ($orders as $order) {            // 查訂單關(guān)聯(lián)的商品(跨中間表orderGoods)            $goodsArr = $order->goods;            // 中間表$order->orderGoods的數(shù)據(jù)在此也被拉回來(lái)            echo count($order->orderGoods) . PHP_EOL;            // 打印每個(gè)商品的名稱            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)
發(fā)現(xiàn)with僅指定goods關(guān)聯(lián),則中間關(guān)聯(lián)orderGoods的查詢也被eager處理了。
簡(jiǎn)單關(guān)聯(lián)之級(jí)聯(lián)和跨中間表關(guān)聯(lián)實(shí)現(xiàn)的功能一致,但是不通過(guò)via實(shí)現(xiàn),而是通過(guò)定義若干級(jí)聯(lián)的1:1或1:n關(guān)聯(lián)來(lái)加載數(shù)據(jù)。
上述中間表關(guān)聯(lián)中,ArOrder是主體,orderGoods和goods都被注入在ArOrder對(duì)象身上,這樣的優(yōu)點(diǎn)是eagerload可以優(yōu)化整個(gè)查詢流程,減少db交互,同時(shí)冗余表達(dá)的goods對(duì)象少(只需要2個(gè)goods對(duì)象,由2個(gè)order共享,下面代碼可以測(cè)試):
$orders[0]->goods[0] === $orders[1]->goods[0]
另一種表達(dá)這種關(guān)系的方式是:arOrder->orderGoods->goods這種間接訪問(wèn)的方式,這樣僅需要維護(hù)arOrder和orderGoods間的1:n關(guān)系以及orderGoods和Goods間的1:1關(guān)系既可,優(yōu)點(diǎn)是訪問(wèn)方式更能體現(xiàn)表關(guān)聯(lián)的間接性,但是缺點(diǎn)就是eagerload無(wú)法完整優(yōu)化整個(gè)流程,同時(shí)goods對(duì)象冗余多。
訂單,商品表 ,商品清單表級(jí)聯(lián)ArOrderGoods添加關(guān)聯(lián):
    public function getGoods() {        return $this->hasOne(ArGoods::className(), ['goods_id' => 'goods_id']);    }		lazyload測(cè)試:
    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`=1SELECT * FROM `ar_goods` WHERE `goods_id`=1SELECT * FROM `ar_goods` WHERE `goods_id`=2SELECT * FROM `ar_order_goods` WHERE `order_id`=2SELECT * FROM `ar_goods` WHERE `goods_id`=1SELECT * FROM `ar_goods` WHERE `goods_id`=2
eagerload測(cè)試:
    public function actionNoVia()    {        // 第一級(jí)關(guān)系eagerload        $orders = ArOrder::find()->with('orderGoods')->all();        foreach ($orders as $order) {            // 第二級(jí)關(guān)系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`=1SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)SELECT * FROM `ar_order_goods` WHERE `order_id`=2SELECT * FROM `ar_goods` WHERE `goods_id` IN (1, 2)
可見(jiàn),級(jí)聯(lián)方式的交互總是比中間表方式要多,內(nèi)存占用也要多,雖然經(jīng)過(guò)eagerload優(yōu)化可以減少幾次交互。
joinWith 多表關(guān)聯(lián)Yii2支持數(shù)據(jù)庫(kù)的join語(yǔ)法,不過(guò)在編程的時(shí)候不是a表join b表這樣的表達(dá)方式,而是a表通過(guò)哪個(gè)關(guān)聯(lián)進(jìn)行join,這個(gè)關(guān)聯(lián)就是我們之前定義的hasOne和hasMany,它們是不需要變動(dòng)的。
不過(guò)Yii2的JOIN并不是你想的那樣:'一句SQL查回所有的關(guān)聯(lián)數(shù)據(jù),填充到關(guān)聯(lián)關(guān)系里',這是非常特殊的地方,文檔里這樣提到:
joinWith()和with()的差別在于前者是聯(lián)合查詢,即通過(guò)把查詢條件應(yīng)用于主表和關(guān)聯(lián)表來(lái)獲取主表記錄,而后者是關(guān)聯(lián)查詢,即只是針對(duì)主表查詢條件獲取主表記錄。
因?yàn)檫@個(gè)差別,你可以應(yīng)用JOIN SQL語(yǔ)句特有的查詢條件。比如你可以通過(guò)限定關(guān)聯(lián)表的條件來(lái)過(guò)濾主表記錄,如上述例子所示。你還可以通過(guò)關(guān)聯(lián)表列值來(lái)對(duì)主表記錄進(jìn)行排序。
說(shuō)白了,joinWith雖然是使用數(shù)據(jù)庫(kù)的join語(yǔ)法實(shí)現(xiàn)的多表聯(lián)查,但是它不會(huì)一次性的將依賴表的數(shù)據(jù)保存起來(lái),與with相比,僅僅是額外提供了一個(gè)根據(jù)依賴表的數(shù)據(jù)過(guò)濾主表數(shù)據(jù)的機(jī)會(huì),依賴表的數(shù)據(jù)依舊會(huì)通過(guò)再次交互的方式進(jìn)行查詢,是不是既失望又好奇呢?
訂單,商品清單,商品 JOIN測(cè)試:
    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`=1SELECT * FROM `ar_goods` WHERE (`goods_id` IN (1, 2)) AND ((`goods_name` != '雪碧'))
分析:
你會(huì)發(fā)現(xiàn),joinWith的確不是我們所想的一次SQL交互拉回所有依賴數(shù)據(jù),而是用于縮小主體數(shù)據(jù)的規(guī)模,這也是為什么后續(xù)拉取依賴的時(shí)候,需要將依賴表的過(guò)濾條件再次套用的原因。
PHP編程通過(guò)最后的例子,我們可以明顯的感受出:ORM背后的行為并不一定是我們預(yù)期的那樣!
所以,當(dāng)我們使用ORM進(jìn)行表關(guān)聯(lián)的時(shí)候,需要認(rèn)真考慮一下是不是裸寫(xiě)SQL的方式性能更佳,但是也別忘記ORM給我們帶來(lái)的抽象性和編程效率。
感興趣請(qǐng)點(diǎn)擊關(guān)注我,歡迎討論。
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。
新聞熱點(diǎn)
疑難解答
圖片精選