當下所有包含數據庫組件的框架,都提供了一套流暢的操作方式去生成查詢語句,這一部分我們稱作
查詢構造器。查詢構造器的存在,使得在數據庫操作這一層面徹底的和原生開發區分開來。原生開發中,查詢語句都是人為地根據業務需求手工寫的,沒有對查詢語句進行規范的封裝(即使有也不是人人能夠做得到且做得好),隨著項目擴展,這種原生的寫法會導致項目愈發難以維護,而且由于操作原始,更加容易引發很多不必要的 BUG 以及安全隱患。查詢構造器針對每一類語句關鍵字進行封裝,通過流暢的鏈式方法組合,形成一個樹狀的數據結構,最終由生成器生成目標查詢語句。我向來認為 Laravel 框架的組件永遠不是最優秀的,但由于其他框架或多或少不優雅、實現差、功能殘缺,才使得 Laravel 的每個組件無論是單個拿出來看還是組合起來看,都是那么的好用,這其中的典型就是
查詢構造器。大多數優秀框架提供的查詢構造器 功能 都已經十分完善,基本上在絕大多數業務上可以替換手工書寫查詢語句,但是對于復雜的查詢語句,很多框架的查詢構造器提供的方法要么是根本無法實現(比如多種條件和條件之間的邏輯運算、嵌套查詢或子查詢),要么是實現的方式不夠優雅和直觀,使得在對于復雜的查詢條件的情況下,不得不回歸原生的 SQL 語句查詢。雖然一部分人認為原生的查詢語句才是最直觀且合適的,亦或者認為復雜的查詢語句就應當使用原生,但是我們不應該孤立在某一類項目或者一類人群下看待這個問題。要知道,對于大型項目,合理的封裝和可維護性,開發的便捷和效率這些因素往往十分重要,查詢構造器的誕生本來就是因為這個原因。
Laravel 給出了一個令人滿意的實現方法,使得之前那些框架的查詢構造器黯然失色(當然,不包括
Doctrine)。清晰地結構
若是不看文檔和任何介紹,你能夠得出下面查詢構造器最終生成的 SQL 語句或者看得出這段代碼的意圖嗎:
<?php$collection = DB::table('articles') ->select(['title', 'name', 'author', 'created_at', 'published_at']) ->where('name', 'like', "%$name%") ->where(function ($query) { $query->where('top', '>', 5) ->orWhere('status', '=', 1); }) ->orderBy('updated_at', 'desc') ->take(5) ->get()相信很多人都非常容易看得出這個代碼的含義,上述代碼僅僅只是個例子,若移步至官方文檔和查詢構造器的 API 文檔,還有更多的方法可以使用。
Laravel 的查詢構造器基本涵蓋了日常所用到的所有語句的可能,也提供了很多有用的封裝,用于細粒的操作。這些大多在文檔里體現,本文不作重點講述。
復雜邏輯下的思維
我不是文檔的搬運工,再詳盡的開發教程也永遠教不會那些只會復制粘貼、亂問問題的人,作為一枚不斷學習的 PHPer,只會用永遠不夠,任何一個易用的框架和組件,思想永遠是首要學習的東西。但愿認真看完以下章節和后續文章的 PHPer 不再會問出一些愚蠢的問題(當然,沒能一下子懂并不是愚蠢,也可能是……你和我的思路不一致,稍微轉換思維或許會發現新的有趣的世界)。
我第一次使用查詢構造器是在運用 ThinkPHP 3.1 開發項目時用到的,當時筆者水平很爛,為了加強學習,我忍著枯燥無味,將 TP 的數據庫組件的源代碼一行一行的吃了個透。雖然現在來看 TP 的實現并不是很優秀,但是依舊給了我無限的幫助,使得我閱讀源碼的能力提升了一個很大的臺階。
回過頭來,有了之前的經驗,我依舊選擇閱讀源碼的方式來學習框架,而不僅僅只依靠文檔,因為我更想知道作者的思路和每一個功能實現的方式,以及為什么會這么用。
我對比了很多個數據庫組件的實現,包括著名的
Doctrine,在很多地方都有大同小異,當然 Laravel 的也不例外,不過一旦考究起細節,才不得不贊嘆,越是國際化的認可度高的,確實有它值得認可的道理。查詢構造器都會提供一種
Chain(鏈式)訪問的方法,這樣使得開發者可以更為直觀的對應目標查詢語句,因此方法名往往和 SQL 語句差距不大(NoSQL 驅動的存在特殊性在此不做討論),但是查詢構造器本身的目的是為了生成查詢語句,因此如何組織這些方法的參數,保證在易于理解和記憶的情況下,又能有著無比強大的功能,便成了考究設計者水平的東西。在一些早期框架,查詢構造器一般都會提供
where、order by、group by等很直觀的的語句對應的方法,尤其是 order by 這類參數結構單一的,十分容易設計,比如第一個參數是排序的字段,后者則是排序方式,亦或者參數是一個數組,數組下有很多項,這樣就可以生成多重排序。不過一旦到了 where 這種結構復雜的情況,傳統的思路就容易吃癟,比如多個條件如何體現?傳統的代碼如下
$query->where(['a' => 'foo', 'b' => 'bar']);這種語句的 where 最終是這樣
WHERE a = 'foo' AND b = 'bar',但是如果我們要這樣WHERE a = 'foo' OR b = 'bar'或者WHERE a > 100 AND b = 'foo'上面的那種通過數組來組織的方式,就很難實現。查詢構造器想要實現上面例子的這個 where 方法,實際上只需要循環參數提供的數組,記錄到查詢構造器對象的一個私有屬性里,在最終生成輸出 SQL 語句的階段,進行拼接即可。可以自己嘗試著實現一個簡單的查詢構造器。
通過數組組織等式,很容易,但數組結構本身存在局限,而且一旦結構復雜,可讀性便會極大地降低,框架的本質是提供一種方便的機制來提升開發效率降低錯誤概率,這樣很顯然背道而馳。有的框架提出了另外一種方案,在不破壞整體的鏈式訪問的結構下,在局部使用原生語句來實現復雜的查詢:
$query->where("a > 100 OR b = 'foo'");這種思路是一個不錯的解決辦法,但是就像我之前提到的,這種方式雖然比直接全盤原生語句好了些,但不可避免這種寫法無法過濾,一旦用于查詢判斷的條件式中有外部變量,就很容易出現注入問題,或許你可以說利用 PDO,以以下方式實現:
$query->where("a > ? OR b = ?", [$parameter1, $parameter2]);但是如果你是框架或組件的作者,你一定會苦惱于另外一件事:這個方法如何適用于上述各種情況(比如簡單條件下的、復雜條件下的等等)?有的人想到了判斷參數類型來實現,比如參數 0 是數組時,就用常規的辦法(上面的 blockquote 有講),若參數 0 是文本,就用原生的方式集成。
TP 就是這么一個思路:
PRotected function parseWhere($where){ $whereStr = ''; if (is_string($where)) { // 直接使用字符串條件 $whereStr = $where; } else { // 使用數組表達式 $Operate = isset($where['_logic']) ? strtoupper($where['_logic']) : ''; if (in_array($operate, array('AND', 'OR', 'XOR'))) { // 定義邏輯運算規則 例如 OR XOR AND NOT $operate = ' ' . $operate . ' '; unset($where['_logic']); } else { // 默認進行 AND 運算 $operate = ' AND '; } // 更多請參考 https://github.com/top-think/thinkphp/blob/master/ThinkPHP/Library/Think/Db/Driver.class.php#L562好了,重點來了,我說過這種方式不是不好,而是還可以更好,因為復合條件存在的可能非常多,基本是常態了,因此我認為常態的形式,就應當進行 簡化 和擺脫原生寫法的困擾,這樣才更為合理。Laravel 封裝的這一系列方法,更為 優雅,相信各位能夠感覺得到,因為你無須過多參閱文檔,都能領會每個調用的含義,直觀、簡潔。更重要的,是在做到直觀簡潔的同時,兼具了強大的功能,使得其在復雜的開發情境下依舊保持原有的風格。
Laravel 的
where方法的參數很多,實際用到的只有前三個參數,WHERE a = x,這個表達式中 “a” 是參數 0,“=” 是參數 1,“b” 是參數 2。在參數 1 為 “=” 時,可以省略,而后直接將參數 2 挪至參數 1 的位置。無論如何,都很直觀。復雜的條件邏輯的實現也很簡單,Laravel 還提供了另外幾種 where 方法:
orWhere、(or)whereIn、(or)whereNotNull、(or)whereNull、(or)whereBetween。看過一次的人基本無需翻閱文檔兩次。更為重要的,Laravel 可以很輕松地實現WHERE (a = x AND b =y) OR c = z這種形式,并且依舊優雅和富有表現力,不再贅述,前文已經體現過了 Laravel 利用匿名函數實現這種括號包裹和子查詢的功能。我們來看看 Laravel 是如何書寫它的 where 代碼的:
public function where($column, $operator = null, $value = null, $boolean = 'and'){ if (is_array($column)) { return $this->addArrayOfWheres($column, $boolean); } if (func_num_args() == 2) { list($value, $operator) = [$operator, '=']; } elseif ($this->invalidOperatorAndValue($operator, $value)) { throw new InvalidArgumentException('Illegal operator and value combination.'); } if ($column instanceof Closure) { return $this->whereNested($column, $boolean); } if (! in_array(strtolower($operator), $this->operators, true) && ! in_array(strtolower($operator), $this->grammar->getOperators(), true)) { list($value, $operator) = [$operator, '=']; } if ($value instanceof Closure) { return $this->whereSub($column, $operator, $value, $boolean); } if (is_null($value)) { return $this->whereNull($column, $boolean, $operator != '='); } $type = 'Basic'; if (Str::contains($column, '->') && is_bool($value)) { $value = new Expression($value ? 'true' : 'false'); } $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); if (! $value instanceof Expression) { $this->addBinding($value, 'where'); } return $this;}上述代碼中有著對另外部分方法的調用,但并不影響我們看出一些端倪。不過 Laravel 的東西拆的太細,如果要將其調用的方法全部展示篇幅會很大,我將會著重分析幾個點。
我們注意到這個
多余的參數是干什么的?為什么要拆分的那么細?Expression 這個類是干嘛的?where方法其中有些和其他框架組件不太一致的設計:這也是這部分的重點。關于這部分,下一篇再講。
關于查詢構造器,我們后幾篇文章中除了分析這個 where 的代碼,還有包括一些承接性質的方法比如
addBinding這些不是很起眼的但卻非常重要的方法,當然也會引入語法生成器的介紹,謝謝各位的關注!感謝博主:https://www.insp.top/article/learn-laravel-query-builder-part-1
新聞熱點
疑難解答