來繼續填坑(上個月沒發,因此這個月至少兩篇)。
上一篇 laravel 學習筆記 —— 查詢構造器(下) 中我們正在開始分析查詢構造器的
where方法,作為出場率最高的方法,其中相關的玩意兒足以幫助我們去理解整個組件大致的設計思路和實現細節。由于整體分析實在是太過麻煩(我其實就是懶),因此我們在上一篇的末尾提了三個問題,本文也將順著這三個問題來進行解析,當然其余的思路類似的,就不在贅述,畢竟精力有限。上一篇提到的問題是:
where 方法中多余的參數是干什么的方法中的代碼為什么要拆分(或者封裝)的那么細 ?ExPRession 這個類是干嘛的 ?我們就從多余的參數是做什么的開始吧。
開始
where方法所包含的參數有 4 個(Laravel 5.1,這幾篇文章都是基于這個和 5.2 兩個版本講述),分別為:$column,$Operator,$value,$boolean。顧名思義,第一個參數必然是指的字段名稱,第二個參數則是操作符,第三個是值,第四個指的是布爾邏輯。除了第一個參數,其余皆包含默認值意味著可選填。從參數名和你們所閱讀 Laravel 文檔中所知曉的,where 的用法基本已經了解的差不多了,但是第四個參數對于各位應該沒在實際項目中使用過,實際上如果你們愿意去翻一翻源碼,比如
orWhere,你就會發現這些方法本質還是調用的where,只是這參數 4 變了,至于為什么封裝那么細,這就是第二個問題,我們后文會一并講。為了方便分析,我還是把 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;}源碼簡析
執行過程的開始
代碼中,首先開始就是判斷參數 1 是否為數組(有的人可能以其他版本作參考,有差異很正常,但原理類似,這里是 5.2 的寫法),若為數組則類似于 TP 框架的處理方案,將數組的 key 作為字段, value 作為值,操作符為
=的 WHERE 語句,布爾邏輯為 AND,例如$query->where(['a' => 100, 'b' => 200]);轉換的結果即為WHERE a = 100 AND b = 200。若參數 1 不是數組,且參數只填寫了兩個,那么操作符默認為
=,并將參數 2 的值用于實際的參數 3。這種使用形式主要用于快速建立 WHERE 中的等值判斷。我們繼續閱讀代碼,可以看到判斷參數 1 是否為
Closure的代碼,若滿足條件則會調用一個叫做whereNested的方法,這個方法的作用十分重要,實現了在上一篇中我們提到的 Laravel 查詢構造器優雅特性中的關鍵部分,即可以在保證生成復雜層級關系的 SQL 語句的前提下,php 部分的代碼調用非常直觀,通俗點說就是能讓語句中 帶括號 ,雖然不是什么不得了的功能,但是其他框架或類似模塊或多或少調用不直觀,甚至對于這種功能還是逃不了原生寫法,在這樣的對比下,就顯得難能可貴。在這里需要強調一點:并不是所有情況都建議和鼓勵使用 PHP 的方式和寫法去替代 SQL 原生語句,考慮到性能以及其他業務情形,在沒有框架或框架性能較低的情況,完全沒必要用復雜的東西去做,怎么快怎么好。至于在本文中對這些特性的講述,更多是幫助 學習 PHP 的朋友,了解一些思想,包括很多 PHP 的新特性和功能函數是如何被巧妙運用并解決問題的,這才是這一系列文章的作用。
關于
whereNested的實現原理,實際和 where 方法的實現原理類似,只是有邏輯變動,設計思想是一樣的,因此繼續分析 where 的實現。為什么拆分那么細
隨著代碼往后閱讀,我們可以看到更多的對于值類型的判斷以及在種種條件下,將處理的東西交由另一個對應的方法處理。在這里就講講之前的問題,拆分這么細的意義。
我們封裝以及再封裝(通俗所說的拆分)代碼的目的有很多,每個人的出發點或多或少都有著區別,但目的不是便于維護(或復用)就是轉換調用。轉換調用的典型就是我們在 where 方法的參數 4,一般是不會用的,我們常常在 where 或 orWhere 中切換,而 orWhere 就是 where 的參數 4 的值為
or而已,但是很顯然的 where(x, x, x, 'or') 和 orWhere(x, x, x) 哪個更直觀一眼便知,轉換調用還往往用于創建方法以及函數的別名,對于一些項目重構和調整 API 規則時常常用到。更多的情況,封裝是為了便于維護,畢竟重復代碼不會總是去每個地方都去改。這時候就存在一個問題,由于這種目的本身就是一個沒有標準的,因此封裝質量參差不齊,出現注入所謂的一切都要封裝的過度現象(最終導致和沒有封裝毫無差別,甚至更糟)。
任何程序都是實現一定功能的。那么我們看到的,Laravel 在做封裝的時候,往往會將一類特定功能的局部代碼進行封裝,而且這些封裝出來的方法和函數, 往往只會做一件事 !這樣的好處就使得在思路上不存在混亂的可能,而且由于只做一件事,給方法命名就按其所做工作命名即可,一眼就知道這行代碼是做什么的,那么便于維護的目的才真的達到了。
調用轉換為 SQL 的開始
我們在閱讀到代碼最后,看到了這樣一行
$this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');。如果你讀的代碼足夠多,你會發現所有框架或類似模塊中實現查詢構造器時,都會有類似的部分,很顯然,我們目的是使用構造器生成 SQL 語句,那么必然要在某個地方記錄我們在程序中的參數作為生成 SQL 語句的依據。
這沒什么神秘的。
Laravel 實現的方式需要保證功能夠強,夠直觀,因此在這里記錄的東西也顯得非常豐富。注意到一個函數沒有,就是
compact,估計讀本文的大多數讀者一般都未曾接觸過,建議各位去文檔上查一下,在這里就不多講。我們來看看在此處記錄的東西分別是什么。首先是 type,type 記錄的是該條 where 語句的類型,如果我們調用的是
where方法,則該值為 "Basic",當然,我們前面還提到了whereNested,如果調用了這個,則 type 為 "Netsed"。不同的 type 則記錄的東西也有很大不同,對于 "Basic",則還需要記錄 column (字段)、operator(操作符)、value(值)、boolean(布爾邏輯)。而另外的比如 Nested 則會記錄 query(子查詢,又是一個 Builder 對象哦)、boolean。
當記錄完這一切,我們看到了另一個方法
addBinding,向這個方法傳遞了 where 語句中的 value 值,這一步是做什么的呢?我們知道,對于 PDO,SQL 語句是可以預編譯的,這樣也很容易防御來自 SQL 語句注入的攻擊,畢竟預編譯將語句和語句中的變量徹底的分離,變量就是變量,不參與語句執行,這樣安全系數大大提高。以上,我們可見到的再使用 PDO 時,我們會先寫類似這樣的 SQL 語句:SELECT * FROM table WHERE a = ? AND b = ?,問號是一個表示該處為變量的位置,在 SQL 執行時傳入,即所謂的參數綁定。回到先前的問題,我們已經可以回答,
addBinding顧名思義就是添加一個參數的綁定,因為最終構造器以及我們后一篇文章中會講述的(語法)生成器所產生的目標 SQL 語句,就是一個這樣的 SQL 語句,在執行時才會傳入參數。而問號的位置必然需要和所包含的值順序對應,因此在添加 where 語句參數至$query->wheres數組時,同時也需要對應一個綁定參數以此對應。額外細節
我們分析了
where這個比較具有代表性的查詢構造器的方法后,其實可以看出查詢構造器的目的就是提供一系列直觀的方法調用,這些方法的參數就是我們用于查詢語句的參數,但是卻不需要我們考慮諸如字段名加`、表名稱的前綴以及子查詢、JOIN 語句的別名或者一些雜七雜八足以使你無限糾結的 SQL 語句細節。查詢構造器在提供了這些方法的同時,還要保證調用足夠直觀和優雅。但最終在實際邏輯中,就是將參數收集并按需要排列組合成一定形式,最后交由(語法)生成器生成目標 SQL 語句。不過這里有個細節,就是如何在這種形式下,依舊能夠使用原生語句?開頭的第三個問題,
Expression類是干什么的?上面那一大段,如果各位仔細讀,就會發現我提到了字段和表名稱。在查詢構造器中,有個方法:select 和 table。對于 select 我們常規就傳遞字段名就好,但對于特別的情形下,比如我們需要這樣
SELECT count(x) as c FROM table,我們很顯然不可以$query->select('count(x) as c'),因為在(語法)生成器階段,會將參數中屬于字段、表名稱做一些處理,例如在查詢語句中存在調用了多個表的情形下,字段x可能會變成,意味著table.xcount(x) as c會變成,當然實際情況不一定如此,但必定會發生類似情況,因為框架不可能將判斷細化到如此到可以區分格式合不合理,那樣性能開銷會更加大。那么如何避免大面積原生語句的情況下在局部使用原生呢?table.count(x) as c
Expression類就是這個用處。在構造這個類時,我們可以將局部的原生語句作為參數傳入,并將其作為如 select、table、where 等一系列查詢構造器方法的值,在最終由(語法)生成器生成目標 SQL 語句時,會判斷是一個普通值還是一個Expression類,若為后者,則不再做轉換處理。
instance of這個語句用處十分廣泛。后面的內容
查詢構造器不在多講,更多內容可舉一反三,因為落到實際原理,真的不復雜,未講到的部分不代表不重要,因為查詢構造器的細節十分多,但是只要愿意去看源碼,很多東西很容易理解,這得益于 Laravel 的代碼無論從命名還是邏輯結構,都看起來賞心悅目,所以,不要看到此就結束探索,繼續看代碼吧。
下一篇預告:查詢構造器是生成語句的開始,真正生成語句是(語法)生成器,生成器和構造器都是一個在簡單原理上,運用巧妙地辦法實現復雜功能的,(語法)生成器最大的特點就是如何適應各種數據庫驅動下的目標語句生成,這關系到一些架構設計的思路,我們下篇慢慢講。
新聞熱點
疑難解答