PHP & MySQL 「數據關聯一對一」的最佳實踐

前言

在開發過程當中,一般會遇到不少 一對一 數據的處理狀況。而不少時候咱們會要取到的是一個列表,而後列表的單條記錄的對應另一張表,來實現業務。好比下面的商品信息商品詳情 兩個表,這裏爲了演示只是使用了基礎字段,實際開發中可能會複雜的多,下方演示代碼中數據庫鏈接使用 PDO 進行處理。php

表結構

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 限制。會是什麼樣子的狀況?laravel

進階

看到這裏,有人可能會想到了另外一種方案來,先查詢列表,而後取出列表裏面的 goods_id 以後使用 in 查詢,而後再循環分配給列表,看代碼。thinkphp

$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_columnarray_key_exists,接下里一一道來,其實在array_column的官方手冊中的咱們就能 Example #2 中就介紹了咱們想要的方法。套用在這裏就是重置goods_detail_list 裏面元素的 key 爲 單個元素下的 goods_id。在後面咱們直接用 array_key_exists 判斷是否存在,而後作出相應的處理就行了。在這裏咱們還能夠作另一個操做,那就是默認值,由於有時候,數據有可能會因對不上,若是查出來直接返回給前端,前端沒有預料到這種狀況沒有作容錯處理就會致使前端頁面崩潰,下面來改寫一下代碼segmentfault

// 在 「進階」 板塊中,咱們用到了 「array_first」 函數,該函數第三個參數能夠直接設置默認值,咱們就很少講了,主要講講最後一個
$goods_detail_default = [
    'content' => '默認內容',
    'id'      => null,
    'goods_id'=> null,
];
foreach($result as &$item){
    $tmp = array_key_exists($item['id'] , $goods_detail_list_by_keys) ? $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 就能搞定,不多會用到相似於這種方案,其實否則,在主流的框架中,默認的解決方案几乎都是這樣處理的,好比LaravelThinkPHP,考慮到的場景會有不少,好比有的時候我只是須要按需取一部分的,或者我須要根據我後面的業務結果來決定是否是要加載一對一,然而這種狀況下 join 彷佛就不太適合。數組

勘誤

感謝評論區 @maclxf 對文章內容錯誤之處的指出。框架

相關文章
相關標籤/搜索