终于被产品的各种刁钻不合常理的需求磨炼出用laravel写出较为优雅的代码,在这里给大家分享一下。
先简单介绍一下基本环境,我们是做一款直播APP的,人很多,所以每个接口都必须尽量优化(主要是SQL的查询)。
有一天,产品跟我们说,那个针对主播的送礼牌行榜能否显示30天内的用户送礼数倒序排列,显示用户是否VIP,用户对主播的亲密度,还有用户的等级。
30天内的数据。也就是说之前那张一直累计数值的排行表不能使用了,而且这个30天是个动态的,也就是说这个数据必须只能利用送礼流水group by出来。我们的送礼流水表是1个月1张表的
介绍一下基本表的情况
用户表user用户资料表user_ext(你大爷的头像竟然放这张表,谁搞的站出来,看我不弄死你)礼物表honey_log_201708(XXXX分表日期)超级VIP表svip亲密度表qinmi(这几张表的关联是无法避免的,加上分页count查询。SQL最优就只能是查询表的数量+1才算是比较合理,laravel完全有足够的能力写出优雅的代码)很多人可能会想到laravel的DB原生查询了么。但是Eloquent有强大的关联,访问器修改,查询范围等等这些功能让你的代码非常简洁。
我们先写model
1.用户表user关键字段是id用户ID,nickanem昵称,exp经验值exp, config('user.level.num')); }}
2.用户资料表
主要字段uid主键,header_name头像文件名,header_lock头像是否被锁(0,1)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过期时间where('expire', '>', LARAVEL_START); }}
4.亲民度qinmi表
主要字段uid,beauty_uid(主播主键),qinmi_num亲密度值qinmi_num, config('qinmi.qinmi.num')); }}
5.好了,重点来了。honey_log表,这个是重点,因为它是分表的,现在我们要封装一个union表的方法,让这个model自动把涉及的分表作为一张表赋予model查询
=', $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写法。
好。我们来看看控制器如何查询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); }}