對於專案中簡單的多條件查詢的一些心得體會

野犭發表於2018-07-13

簡單記錄下今天專案中遇到一些問題。

首先簡化一下場景:

現有 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......

所以,我將程式碼改寫成這樣是否有意義,是否本末倒置了呢?

第一次發文,歡迎各位拍磚扔雞蛋,吾必洗耳恭聽之。

你可以侮辱我的事業,但不能侮辱我的人,額,反過來也一樣

I guess it comes down to a simple choice:get busy living or get busy dying.

相關文章