终于被产品的各类刁钻不合常理的需求磨炼出用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); } }