簡單記錄下今天專案中遇到一些問題。
首先簡化一下場景:
現有 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......
所以,我將程式碼改寫成這樣是否有意義,是否本末倒置了呢?
第一次發文,歡迎各位拍磚扔雞蛋,吾必洗耳恭聽之。
你可以侮辱我的事業,但不能侮辱我的人,額,反過來也一樣