You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 18:31:22