laravel較優雅的分表關聯查詢(性能較好,SQL的數量=表的數量,涵蓋了較多laravel手冊推薦的方法)

終於被產品的各類刁鑽不合常理的需求磨鍊出用laravel寫出較爲優雅的代碼,在這裏給你們分享一下。php

先簡單介紹一下基本環境,咱們是作一款直播APP的,人不少,因此每一個接口都必須儘可能優化(主要是SQL的查詢)。前端

有一天,產品跟咱們說,那個針對主播的送禮牌行榜可否顯示30天內的用戶送禮數倒序排列,顯示用戶是否VIP,用戶對主播的親密度,還有用戶的等級。laravel

30天內的數據。也就是說以前那張一直累計數值的排行表不能使用了,並且這個30天是個動態的,也就是說這個數據必須只能利用送禮流水group by出來。咱們的送禮流水錶是1個月1張表的web

介紹一下基本表的狀況
用戶表user
用戶資料表user_ext(你大爺的頭像居然放這張表,誰搞的站出來,看我不弄死你)
禮物表honey_log_201708(XXXX分表日期)
超級VIP表svip
親密度表qinmi
(這幾張表的關聯是沒法避免的,加上分頁count查詢。SQL最優就只能是查詢表的數量+1纔算是比較合理,laravel徹底有足夠的能力寫出優雅的代碼)sql

不少人可能會想到laravel的DB原生查詢了麼。可是Eloquent有強大的關聯,訪問器修改,查詢範圍等等這些功能讓你的代碼很是簡潔。json

咱們先寫model
1.用戶表user
關鍵字段是id用戶ID,nickanem暱稱,exp經驗值數組

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $table = 'user';
 
    /**這個是laravel的訪問器方法,實際user表是沒有等級這個字段的
    可是能夠在這裏定義出等級是怎麼來的,給user添加了level這個屬性**/
    public function getLevelAttribute()
    {
        //用戶的經驗值根據配置文件的等級區間計算用戶等級,section是本身封裝函數
        return section($this->exp, config('user.level.num'));
    }
}

2.用戶資料表
主要字段uid主鍵,header_name頭像文件名,header_lock頭像是否被鎖(0,1)緩存

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class UserExt extends Model
{

    protected $table = 'user_ext';

    protected $primaryKey = 'uid';

    
    //添加頭像屬性
    public function getHeaderUrlAttribute()
    {
        //頭像放在了cdn上,先判斷一下頭像是空的話給默認,或者頭像被管理員鎖了的話也給默認
        if ($this->header_lock == 1 || $this->header_name == '') {
            $headerUrl = 'http://www.cdn.com/' . 'default_header_user.png';
        } else {
            $headerUrl = 'http://www.cdn.com/' . $this->header_name;
        }
        return $headerUrl;
    }
}

3.SVIP表
主要字段uid主鍵,expire過時時間函數

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Svip extends Model
{
    protected $table = 'svip';
    
    protected $primaryKey = 'uid';

    //這裏用到了laravel的查詢範圍,能夠快速調用判斷是否VIP,不用每次都寫一次where
    public function scopeValidVip($query)
    {
        return $query->where('expire', '>', LARAVEL_START);
    }
}

4.親民度qinmi表
主要字段uid,beauty_uid(主播主鍵),qinmi_num親密度值優化

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Qinmi extends Model
{
    protected $table = 'qinmi';
    
    public function getLevelAttribute()
    {
        //轉換親密度等級                
        return section($this->qinmi_num, config('qinmi.qinmi.num'));
    }
}

5.好了,重點來了。honey_log表,這個是重點,由於它是分表的,如今咱們要封裝一個union表的方法,讓這個model自動把涉及的分表做爲一張表賦予model查詢

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use DB;

class HoneyLog extends Model
{
    /*傳入查詢的開始日期和結束日期用於計算跨越的表和達到約束表數據的目的。
    外部能夠調整查詢的列,還能夠添加where條件*/
    public function setUnionAllTable($startTime = LARAVEL_START, $endTime = LARAVEL_START, $attributes = ['*'], $wheres = [])
    {
        //約束條件
        $whereConditions = [];
        $wheres = array_merge([['time', '>=', $startTime], ['time', '<', $endTime]], $wheres);
        //時間戳轉日期
        $startDate = date('Y-m', $startTime);
        $endDate = date('Y-m', $endTime);
        //涉及的表數組
        $tables = [];
        //循環where數組,格式是[['字段','表達式','值',' and|or '],['字段','表達式','值',' and|or ']]
        //例子[['beauty_uid', '=', '2011654', 'and']]
        foreach ($wheres as $val) {
            //組裝每一個where條件
            $val[2] = $val[2] ? $val[2] : "''";
            if (isset($val[3])) {
                $whereConditions[] = " {$val[3]} {$val[0]} {$val[1]} {$val[2]}";
            } else {
                $whereConditions[] = " and {$val[0]} {$val[1]} {$val[2]}";
            }
        }
        //循環開始日期和結束日期計算跨越的表
        for ($i = $startDate; $i <= $endDate; $i = date('Y-m', strtotime($i . '+1month'))) {
            $tables[] = 'select ' . implode(',', $attributes) . ' from cdb_honey_log_' . date('Yn', strtotime($i)) . ' where 1' . implode('', $whereConditions);
        }
        //會獲得每個表的子查詢,由於都有約束條件,因此每個子查詢得結果集都不會不少
        //用setTable的方法把這個子查詢union all 後 as一個表名做爲model的table屬性
        //sql大概會是:(select xxx,xxx from honey_log_20177 where time >= 開始日期 and time < 結束日期 and xxx union all select xxx,xxx from honey_log_20178 where time >= 開始日期 and time < 結束日期 and xxx) as cdb_honey_log
        //核心是看你輸入的開始日期和結束日期和約束條件,組裝成一個union all的子查詢而後做爲table賦予model
        return $this->setTable(DB::raw('(' . implode(' union all ', $tables) . ') as cdb_honey_log'));
    }

    //關聯用戶資料表,要拿頭像
    public function userExt()
    {
        return $this->belongsTo(UserExt::class, 'uid');
    }
    //關聯用戶表,要拿暱稱
    public function user()
    {
        return $this->belongsTo(User::class, 'uid');
    }
    //關聯SVIP表,要判斷是否VIP
    public function svip()
    {
        return $this->belongsTo(Svip::class, 'uid');
    }
    //關聯用戶對於主播的親民值
    public function qinmi()
    {
        return $this->hasMany(Qinmi::class, 'uid', 'uid');
    }
    //轉化送禮等級,按送禮金額轉化
    public function getHoneyLevelAttribute()
    {
        return section($this->honey_num, config('beauty.honey.num'));
    }
}

以上準備工做都有了。相信熟悉laravel的人已經知道怎麼查詢了,能夠達到最優化的SQL,和最優雅的laravel寫法。
好。咱們來看看控制器如何查詢

<?php
namespace App\Http\Api\Controllers;


use Illuminate\Http\Request;
use DB;
use Cache;
use Carbon\Carbon;
use App\Models\HoneyLog;


class AAHoneyLogController extends Controller
{
    public function index(Request $request)
    {
        // 主播ID
        $beauty_uid = $request->input('beauty_uid');
        // 每頁顯示數量
        $pageSize = $request->input('pagesize', 10);
        // 當前頁
        $page = $request->input('page');
        // 緩存數據,按查詢的主播,頁數做爲key分頁
        $data = Cache::remember("user_for_beauty_rank_{$beauty_uid}_{$pageSize}_{$page}", 2, function () use ($beauty_uid, $pageSize, $page) {
            // 計算30天前
            $startTime = Carbon::today()->subDays(30)->getTimestamp();
            // 計算結束日期
            $endTime = Carbon::tomorrow()->getTimestamp();
            // 實例化honeyLog模型,由於自定義的setUnionAllTable方法是非靜態方法,若是誰知道如何在model定義非靜態方法可是能夠經過靜態調用的話,請告訴我,由於不想改底層,laravel是用了魔法靜態方法實例化調用的,因此咱們纔可使用model::select()->where()->get()這樣的鏈式調用,可是在model本身定義的實體方法好像並無繼承到這種調用
            $honeyLog = new HoneyLog;
            // 查詢該主播ID30天有親密值的用戶group by 排序 用分頁paginate
            $lists = $honeyLog->setUnionAllTable($startTime, $endTime, ['uid', 'honey_num'], [['beauty_uid', '=', $beauty_uid]])
                ->select(DB::raw('uid, sum(honey_num) as honey_num'))->groupBy('uid')->orderBy('honey_num', 'desc')->paginate($pageSize);
            // 不少人可能會問爲何不用with()渴求式加載,由於用了with的話,model會默認去構造一次實例,致使table屬性丟失,大家試試就知道了,因此下面咱們終於理解到laravel爲何會還有個懶惰渴求式加載了,簡直絕配
            
            // 懶惰渴求式加載頭像,vip,親密值,暱稱
            // 好好理解下面的關聯約束
            $lists->load([
                'userExt' => function ($query) {
                    $query->select('uid', 'header_name', 'header_lock');
                },
                'user' => function ($query) {
                    $query->select('bid', 'nickname', 'exp');
                },
                'svip' => function ($query) {
                    // 這個validVip是模型定義的範圍約束方法,至關於where('expire', '>', LARAVEL_START)
                    $query->select('uid')->validVip();
                },
                'qinmi' => function ($query) use ($beauty_uid) {
                    // 這裏須要傳入主播ID,只查找用戶對於這個主播的親密值
                    $query->select('uid', 'qinmi_num')->where('beauty_uid', $beauty_uid);
                }
            ]);

            // 如今須要的數據都已經所有查出來了,因爲我作的是API,如今要組裝前端須要的格式return出去就能夠了,
            // 若是是本身作的web網頁,就直接丟給視圖遍歷就能夠了

            $result = [];
            foreach ($lists as $key => $value) {
                $result[] = [
                    // 用戶id
                    'uid' => $value->uid,
                    // 送禮數量
                    'honey_num' => $value->honey_num,
                    // 頭像
                    'header' => $value->userExt->header_url,
                    // 是否vip
                    'svip' => $value->svip ? 1 : 0,
                    // 送禮等級
                    'honey_level' => $value->honeyLevel,
                    // 親密等級
                    'qinmi_level' => $value->qinmi->isEmpty() ? 0 : $value->qinmi[0]->level,
                    // 暱稱
                    'nickname' => $value->user->nickname,
                    // 用戶等級
                    'level' => $value->user->level,
                ];
            }

            // 這是前端要求的格式,要這樣組裝沒有什麼特別要說的,只是前端習慣這樣的結構
            $data = [
                'page' => [
                    'last_page' => $lists->lastPage(),
                    'current_page' => $lists->currentPage(),
                    'list' => $result,
                ],
            ];

            return $data;
        });
        
        return response()->json($data);
    }

}
相關文章
相關標籤/搜索