如何在Laravel Eloquent中使用自定义属性执行查询统计?
问题描述
我有一个Student模型,数据库中存储了学生的社保号,通过自定义属性age从社保号计算学生年龄。现在需要在Eloquent查询中使用该属性统计特定年龄段的学生数量,避免加载所有模型带来的性能问题。
模型中已定义的自定义属性:
public function getAgeAttribute(): int { if (!$this->deleted) { $date = substr($this->social_security, 0, 6); switch (substr($this->social_security, 6, 1)) { case '-': $yy = '19'; break; case 'A': $yy = '20'; break; case '+': $yy = '18'; break; default: return 0; } $birthday = new \DateTime($yy . substr($date, 4, 2) . '-' . substr($date, 2, 2) . '-' . substr($date, 0, 2)); $today = new \DateTime('today'); return $birthday->diff($today)->y; } return 0; }
当前实现会加载所有符合条件的模型,性能较差:
$count = Student::where('deleted', 0) ->whereHas('studentSeasons', function($query) { $query->whereHas('season', function($query) { $query->where('start', '<=', now()); $query->where('end', '>=', now()); }); }) ->get() ->append('age') ->where('age', '>=', $request->input('start')) ->where('age', '<=', $request->input('end')) ->count();
解决方案
方法一:用SQL表达式直接在查询中计算年龄
将PHP中从社保号计算年龄的逻辑转换成SQL表达式,通过whereRaw直接在数据库层面完成年龄计算与筛选,无需加载所有模型。以MySQL为例:
$startAge = $request->input('start'); $endAge = $request->input('end'); $count = Student::where('deleted', 0) ->whereHas('studentSeasons', function($query) { $query->whereHas('season', function($query) { $query->where('start', '<=', now()); $query->where('end', '>=', now()); }); }) ->whereRaw(" TIMESTAMPDIFF(YEAR, STR_TO_DATE( CONCAT( CASE SUBSTRING(social_security, 7, 1) WHEN '-' THEN '19' WHEN 'A' THEN '20' WHEN '+' THEN '18' ELSE '00' END, SUBSTRING(social_security, 5, 2), '-', SUBSTRING(social_security, 3, 2), '-', SUBSTRING(social_security, 1, 2) ), '%Y-%m-%d' ), CURDATE() ) BETWEEN ? AND ? ", [$startAge, $endAge]) ->count();
逻辑说明:
SUBSTRING(social_security, 7, 1)提取社保号第7位的年份标识CASE语句对应PHP中的switch逻辑,生成年份前缀(18/19/20)CONCAT拼接出YYYY-MM-DD格式的生日字符串STR_TO_DATE将字符串转换为数据库可识别的日期类型TIMESTAMPDIFF(YEAR, 生日, CURDATE())计算当前年龄- 参数绑定避免SQL注入风险
方法二:添加数据库生成列(推荐)
如果你的数据库支持生成列(MySQL 5.7+、PostgreSQL 12+等),可以在students表中添加自动计算年龄的生成列,后续查询可直接使用该列,简化代码同时提升性能。
步骤1:执行迁移添加生成列
以MySQL为例,编写迁移文件:
Schema::table('students', function (Blueprint $table) { $table->integer('age')->generatedAs(" TIMESTAMPDIFF(YEAR, STR_TO_DATE( CONCAT( CASE SUBSTRING(social_security, 7, 1) WHEN '-' THEN '19' WHEN 'A' THEN '20' WHEN '+' THEN '18' ELSE '00' END, SUBSTRING(social_security, 5, 2), '-', SUBSTRING(social_security, 3, 2), '-', SUBSTRING(social_security, 1, 2) ), '%Y-%m-%d' ), CURDATE() ) ")->stored(); // 存储列:社保号更新时自动重新计算;也可使用virtual()虚拟列,查询时实时计算 });
步骤2:更新模型配置
在Student模型中添加该字段(若不需要批量赋值可跳过fillable配置):
protected $fillable = [..., 'age']; // 若原自定义属性age不再需要,可移除$appends中的配置
步骤3:简化查询代码
现在可以直接用普通的where条件筛选年龄:
$startAge = $request->input('start'); $endAge = $request->input('end'); $count = Student::where('deleted', 0) ->whereHas('studentSeasons', function($query) { $query->whereHas('season', function($query) { $query->where('start', '<=', now()); $query->where('end', '>=', now()); }); }) ->where('age', '>=', $startAge) ->where('age', '<=', $endAge) ->count();
优势:代码简洁,性能更优,还可以给age列添加索引进一步提升查询速度。注意不同数据库的生成列语法略有差异,需根据实际使用的数据库调整。
内容的提问来源于stack exchange,提问作者riksae
相关产品推荐
相关产品推荐

