对于项目中简单的多条件查询的一些心得体会
简单记录下今天项目中遇到一些问题。
首先简化一下场景:
现有 projects
和 service_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']);
}
这样写有两个问题:
- 当有字段为空时会进行一些不必要的查询;
- 不能在 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 协议》,转载必须注明作者和本文链接
我也来分享一下自己的写法。:smile:
使用的时候
@沈益飞 嗯嗯,感觉比我写的通用性更强,而且代码也非常简洁,受教了,佩服佩服 :smile:
可以参考下 yii2 的 andFilterWhere():
如果 只是传了 email password 字段,它会生成 where email = :1 and password =:2
相同,如果只传了 phone password 字段,它会生成 where phone =:1 and password =:2
可以自己实现下这个,挺方便的。
@freewill 嗯嗯,刚翻了一下 yii2 的文档,这个方法确实不错 :smile:
可以看看这个:https://github.com/codekerala/Laravel-5.6-...
@科大大 感谢推荐,有认真看,学习了 :smile:
@沈益飞 为什么使用 Collect->each 而不使用数组函数操作?
你好,在这个帖子中,如果有字段类型为boolean 的,例如商品上架/ 下架 状态。上架 value=1 下架 value=0,传到你的
if ($filterData[$key]) {
// 使用驼峰的形式将方法名动态写出来
$query->{camel_case($key) . 'Filter'}($value);
}
的时候,会发现,会屏蔽掉value=0 的查找。
@joylee109 感谢指正,确实是有这样的问题 修改为
!is_null($filterData[$key])
这样就好了 smiley: