对于项目中简单的多条件查询的一些心得体会

简单记录下今天项目中遇到一些问题。

首先简化一下场景:

现有 projectsservice_types 两个表以及对应模型,模型之间为 多对多 关联。

项目表 projects 主要结构如下:

字段 类型 索引 描述
id unsigned int 主键 ID
user_id unsigned int 外键 所属用户 ID
name string 项目名称
linkman string 联系人
created_at datetime 创建时间
project_started_at datetime 项目开始时间
project_ended_at datetime 项目结束时间
status string default start 项目状态
... ... ... ...

服务类型表 service_types 结构如下:

字段 类型 索引 描述
id unsigned int 主键 ID
name string 服务类型名称

projects.index 页面中,有如下几个搜索条件:

  • 项目 ID
  • 项目名称 name
  • 联系人 linkman
  • 项目开始时间 project_started_at
  • 项目结束时间 project_ended_at
  • 项目状态 tab
  • 服务类型 service_type

其中,项目状态tab 标签形式, 服务类型select 下拉列表 形式。

——————————————我是不知道怎么分割的分割线——————————————

想查询出 当前登录用户 下的指定条件的项目,根据如上条件编写代码:

ProjectController

use App\Models\Project;
use App\Models\ServiceType;
use Illuminate\Http\Request;

class ProjectController extends Controller
{
    public function index(Request $request, Project $project)
    {
        // tab 为指定项目状态,分别有
        // 待审核 start
        // 运行中 run
        // 已结项 end
        // 以静态属性的形式定义在 Project 模型中
        $filterData = $request->except(['tab']);

        // dd($filterData);
        // array:8 [▼
        //   "id" => null
        //   "name" => null
        //   "linkman" => null
        //   "service_type" => null
        //   "project_started_at" => null
        //   "project_ended_at" => null
        // ]

        // 获取所有服务类别
        $serviceTypes = ServiceType::all();

        // 首先根据用户和项目状态查询
        // 其次根据过滤条件查询
        // 最后分页
        $projects = $project->withStatus($request->user()->id, $request->tab)
            ->withFilter($filterData)
            ->paginate(20);

        return view('groups.projects.index', compact('serviceTypes', 'projects'));
    }
}

Project

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    const STATUS_START = 'start';
    const STATUS_RUN = 'run';
    const STATUS_END = 'end';

    public static $status = [
        self::STATUS_START => '立项',
        self::STATUS_RUN => '运行',
        self::STATUS_END => '结项'
    ];

    public function serviceTypes()
    {
        return $this->belongsToMany(ServiceType::class);
    }

    public function scopeWithStatus($query, $userId, $status = null)
    {
        $query = $query->where('user_id', $userId);

        switch ($status) {
            case self::STATUS_START:
                $query->statusStart();
                break;
            case self::STATUS_RUN:
                $query->statusRun();
                break;
            case self::STATUS_END:
                $query->statusEnd();
                break;
            default:
                $query->statusStart();
        }

        return $query->latest();
    }

    /**
     * 按照过滤条件查询
     */
    public function scopeWithFilter($query, $filterData)
    {
        foreach ($filterData as $key => $value) {
            if (!is_null($filterData[$key])) {
                $query->{camel_case($key) . 'Filter'}($value);
            }
        }

        return $query;
    }

    public function scopeStatusStart($query)
    {
        return $query->where('project_status', self::STATUS_START);
    }

    public function scopeStatusRun($query)
    {
        return $query->where('project_status', self::STATUS_RUN);
    }

    public function scopeStatusEnd($query)
    {
        return $query->where('project_status', self::STATUS_END);
    }

    public function scopeIdFilter($query, $id)
    {
        return $query->where('id', $id);
    }

    public function scopeNameFilter($query, $name)
    {
        $like = '%' . $name . '%';
        return $query->where('name', 'like', $like);
    }

    public function scopeLinkmanFilter($query, $linkman)
    {
        $like = '%' . $linkman . '%';
        return $query->where('linkman', 'like', $like);
    }

    public function scopeServiceTypeFilter($query, $serviceType)
    {
        return $query->whereHas('serviceTypes', function ($query) use ($serviceType) {
            $query->where('service_types.id', $serviceType);
        });
    }

    public function scopeProjectStartedAtFilter($query, $projectStartedAt)
    {
        return $query->where('project_started_at', '>=', $projectStartedAt);
    }

    public function scopeProjectEndedAtFilter($query, $projectEndedAt)
    {
        return $query->where('project_ended_at', '<=', $projectEndedAt);
    }
}

代码打完了,也跑通了,接下来开始思考为什么要这样写。

一开始 Project 模型的 scopeWithFilter 方法我是这样写的:

    public function scopeWithFilter($query, $filterData)
    {
        return $query->where('id', $filterData['id'])
            ->where('name', 'like', '%' . $filterData['name'] . '%')
            ->where('linkman', 'like', '%' . $filterData['linkman'] . '%')
            ->whereHas('serviceTypes', function ($query) use ($filterData['service_type']) {
                // 这里有个坑,一开始写的是
                // $query->where('id', $serviceType);
                // 但是系统会报错
                // 查看 SQL 语句后发现,在联表查询中,service_types 中的字段 id
                // 与projects 中的字段 id 发生冲突,所以应在 id 前加入表名
                $query->where('service_types.id', $filterData['service_type']);
            })
            ->where('project_started_at', '>=', $filterData['project_started_at'])
            ->where('project_ended_at', '<=', $filterData['project_ended_at']);
    }

这样写有两个问题:

  1. 当有字段为空时会进行一些不必要的查询;
  2. 不能在 datetime 类型的字段中进行 null 的比较,如果 $filterData['project_started_at'] 为空的话,系统会报 Illegal operator and value combination. 的错误。

于是对代码进行更改,加入关键词判断:

    public function scopeWithFilter($query, $filterData)
    {
        if ($filterData['id']) {
            // Some codes
        }
        if ($filterData['name']) {
            // Some codes
        }
        if ($filterData['linkman']) {
            // Some codes
        }
        .
        .
        .

        return $query;
    }

这样写是没错了,但看起来不是那么优雅,于是再对代码进行优化:

    public function scopeWithFilter($query, $filterData)
    {
        foreach ($filterData as $key => $value) {
            if ($filterData[$key]) {
                // 使用驼峰的形式将方法名动态写出来
                $query->{camel_case($key) . 'Filter'}($value);
            }
        }

        return $query;
    }
    .
    .
    .

    // 后面将每个方法都拆分开,写成本地作用域的形式

就成了最终的代码,看上去感觉好了不少,哈哈~

But,这里又有了一个问题,本地作用域 的作用是 定义通用的约束集合以便在应用中复用,可上述除了『状态』条件外,其他的条件并非是『通用型』的,所以感觉这里用上 本地作用域 并没有什么卵用......

收尾

上面涉及到的知识点比较基础,主要就是想在自己想法的基础上听听大家的更好的想法,因为多字段搜索是平日做项目比较常用的功能,在 L05 电商教程 - leo 的回答 中,leo 也简单提及了一些搜索的解决方案,在这里先期待一下下一本的进阶书,希望更早学到更专业的搜索方案。

P.S.

一个合格的程序员不仅要有敏捷的思维,还要有良好的编码习惯,emmmmm......

所以,我将代码改写成这样是否有意义,是否本末倒置了呢?

第一次发文,欢迎各位拍砖扔鸡蛋,吾必洗耳恭听之。

你可以侮辱我的事业,但不能侮辱我的人,额,反过来也一样

本作品采用《CC 协议》,转载必须注明作者和本文链接
I guess it comes down to a simple choice:get busy living or get busy dying.
本帖由系统于 2年前 自动加精
野犭
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 9
沈益飞

我也来分享一下自己的写法。:smile:

<?php

namespace App\Models\Traits;

use Illuminate\Database\Eloquent\Builder;

trait Filters
{
    /**
     * 过滤
     *
     * @param Builder $query
     * @param [type] $attributes
     * @return void
     */
    public function scopeFilters(Builder $query, $attributes)
    {
        collect($attributes)->each(function ($attr) use ($query) {
            $query->myFilter($attr);
        });

        return $query;
    }

    /**
     * 单个过滤
     *
     * @param Builder $query
     * @param [type] $attr
     * @return void
     */
    public function scopeMyFilter(Builder $query, $attr)
    {
        return $query->when(request($attr[0], false), function ($query, $value) use ($attr) {
            if ($attr[1] === 'like') {
                $value = "%{$value}%";
            }

            $query->where($attr[0], $attr[1], $value);
        });
    }
}

使用的时候

$users = User::filters([['name', 'like'], ['mobile', '='], ['remark', 'like']])->latest()->paginate(20);
2年前 评论
Bing_c 1年前
野犭

@沈益飞 嗯嗯,感觉比我写的通用性更强,而且代码也非常简洁,受教了,佩服佩服 :smile:

2年前 评论

可以参考下 yii2 的 andFilterWhere():

andFilterWhere(

['email' => $post['email']],

['phone' => $post['phone']],

['password' => $post['password']]

)

如果 只是传了 email password 字段,它会生成 where email = :1 and password =:2
相同,如果只传了 phone password 字段,它会生成 where phone =:1 and password =:2
可以自己实现下这个,挺方便的。

2年前 评论
野犭

@freewill 嗯嗯,刚翻了一下 yii2 的文档,这个方法确实不错 :smile:

2年前 评论
野犭

@科大大 感谢推荐,有认真看,学习了 :smile:

2年前 评论

@沈益飞 为什么使用 Collect->each 而不使用数组函数操作?

2年前 评论

你好,在这个帖子中,如果有字段类型为boolean 的,例如商品上架/ 下架 状态。上架 value=1 下架 value=0,传到你的

if ($filterData[$key]) {
// 使用驼峰的形式将方法名动态写出来
$query->{camel_case($key) . 'Filter'}($value);
}
的时候,会发现,会屏蔽掉value=0 的查找。

2年前 评论
野犭

@joylee109 感谢指正,确实是有这样的问题 修改为 !is_null($filterData[$key]) 这样就好了 smiley:

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!