前言
在開發過程中,通常會遇到很多 一對一
數據的處理情況。而很多時候我們會要取到的是一個列表,然後列表的單條記錄的對應另外一張表,來實現業務。比如下面的商品信息
和 商品詳情
兩個表,這裏爲了演示只是使用了基礎字段,實際開發中可能會複雜的多,下方演示代碼中數據庫連接使用 PDO
進行處理。
表結構
goods
列 | 類型 | 註釋 |
---|---|---|
id | int(11) 自動增量 | 主鍵ID |
title | varchar(100) | 商品名稱 |
price | decimal(10,2) | 商品價格 |
cover | varchar(100) | 商品封面 |
goods_detail
列 | 類型 | 註釋 |
---|---|---|
id | int(11) 自動增量 | 主鍵ID |
goods_id | int(11) | 商品ID |
content | varchar(5000) | 商品圖文介紹 |
初級
坦言,無論是在公司,還是在一些開源項目上,我都看到過如下的代碼。
$query = $db->query('select * from goods');
$result = $query->fetchAll();
// 方案一
foreach($result as $key => $item){
$query = $db->query('select * from goods_detail where goods_id=' . $item['id']);
$result[$key]['goods_detail'] = $query->fetch();
}
var_dump($result);
// 方案二
foreach($result as &$item){
$query = $db->query('select * from goods_detail where goods_id=' . $item['id']);
$item['goods_detail'] = $query->fetch();
}
unset($item);
var_dump($result);
// 方案三
$result = array_map(function($item){
$query = $db->query('select * from goods_detail where goods_id=' . $item['id']);
$item['goods_detail'] = $query->fetch();
return $item;
},$result);
var_dump($result);
這是最暴力的方式,也是立杆見影,而且方案一
看起來代碼貌似還很繁瑣,不是嗎?如果學過 引用這一節的朋友,應該知道第二種用法,直接用引用去操作源數據,當然最後最好別忘了 unset 掉 $item,除了第二種,我們還可以用第三種方式,使用 array_map,誠然,這和第二種方式沒什麼區別,但是這其中有着一個非常大的問題:數據庫查詢的N+1
。從執行中我們就可以看到,除了查詢列表的一條 SQL 外,每查詢一條記錄對應的都需要執行一條 SQL ,導致了額外的查詢,想想一下如果查詢沒有 limit
限制。會是什麼樣子的情況?
進階
看到這裏,有人可能會想到了另一種方案來,先查詢列表,然後取出列表裏面的 goods_id
之後使用 in
查詢,然後再循環分配給列表,看代碼。
$goods_id = array_column($result,'id');
$goods_id_str = implode(',',$goods_id);
$query = $db->query(sprintf('select * from goods_detail where goods_id in (%s)',$goods_id_str));
$goods_detail_list = $query->fetchAll();
foreach($result as &$item){
$item['goods_detail'] = array_first($goods_detail_list,function($item1){
return $item['id'] == $item1['goods_id'];
});
}
unset($item);
var_dump($result);
/**
* 來自 Laravel
*/
if (!function_exists('value')) {
function value($value)
{
return $value instanceof Closure ? $value() : $value;
}
}
/**
* 來自 Laravel
*/
if (!function_exists('array_first')) {
/**
* @param $array
* @param callable|null $callback
* @param null $default
* @return mixed
*/
function array_first($array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
if (empty($array)) {
return value($default);
}
foreach ($array as $item) {
return $item;
}
}
foreach ($array as $key => $value) {
if (call_user_func($callback, $value, $key)) {
return $value;
}
}
return value($default);
}
}
在這個代碼中,我們完美避開了 N+1
的窘境,使用了in
查詢,然後遍歷數組,再使用 array_first
方法來查找後傳遞給 goods_detail
索引,雖然這樣的效率相比第一次的要高了很多,但是並不完美,接下來來看最後一種方案。關於 array_first 可以看我的另一篇文章 『PHP 多維數組中的 array_find』。
最佳實踐
$goods_detail_list_by_keys = array_column($goods_detail_list,null,'goods_id');
foreach($result as &$item){
$item['goods_detail'] = array_key_exists($goods_detail_list_by_keys,$item['id']) ? $goods_detail_list_by_keys[$item['id']] : null ;
// php 7.1+
// $item['goods_detail'] = $goods_detail_list_by_keys[$item['id']] ?? null;
}
unset($item);
var_dump($result);
這一次,我們用到了其他兩個函數。array_column 、 array_key_exists,接下里一一道來,其實在array_column
的官方手冊中的我們就能 Example #2 中就介紹了我們想要的方法。套用在這裏就是重置goods_detail_list
裏面元素的 key 爲 單個元素下的 goods_id
。在後面我們直接用 array_key_exists
判斷是否存在,然後做出相應的處理就好了。在這裏我們還可以做另外一個操作,那就是默認值,因爲有時候,數據有可能會因對不上,如果查出來直接返回給前端,前端沒有預料到這種情況沒有做容錯處理就會導致前端頁面崩潰,下面來改寫一下代碼
// 在 「進階」 板塊中,我們用到了 「array_first」 函數,該函數第三個參數可以直接設置默認值,我們就不多講了,主要講講最後一個
$goods_detail_default = [
'content' => '默認內容',
'id' => null,
'goods_id'=> null,
];
foreach($result as &$item){
$tmp = array_key_exists($goods_detail_list_by_keys,$item['id']) ? $goods_detail_list_by_keys[$item['id']] : [] ;
// php 7.1+
// $tmp = $goods_detail_list_by_keys[$item['id']] ?? [];
$item['goods_detail'] = array_merge($goods_detail_default,$tmp);
}
unset($item);
var_dump($result);
結束
看到這裏就算是完結了但是有的朋友會說,爲什麼不用 leftJoin
來處理?確實,在處理一對一關係中
很多時候我們都會選擇 innerJoin
或者 leftJoin
來進行處理,一條 SQL 就能搞定,很少會用到類似於這種方案,其實不然,在主流的框架中,默認的解決方案几乎都是這樣處理的,比如Laravel、ThinkPHP,考慮到的場景會有很多,比如有的時候我只是需要按需取一部分的,或者我需要根據我後面的業務結果來決定是不是要加載一對一,然而這種情況下 join
似乎就不太適合。