上一篇寫到Eloquent ORM的基類Builder類,此次就來看一下這些方便的ORM方法是如何轉換成sql語句運行的。php
首先仍是進入\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php這個類中,先來看一下最經常使用的where()方法。laravel
以下所示,where方法的代碼很長,但前面多個if都是用來兼容各類不一樣調用式的。咱們先拋開這些花哨的調用方式,來看一下最簡單的調用方法是怎麼運行的。sql
1 /** 2 * Add a basic where clause to the query. 3 * 4 * @param string|array|\Closure $column 5 * @param mixed $operator 6 * @param mixed $value 7 * @param string $boolean 8 * @return $this 9 */ 10 public function where($column, $operator = null, $value = null, $boolean = 'and') 11 { 12 // If the column is an array, we will assume it is an array of key-value pairs 13 // and can add them each as a where clause. We will maintain the boolean we 14 // received when the method was called and pass it into the nested where. 15 if (is_array($column)) { 16 return $this->addArrayOfWheres($column, $boolean); 17 } 18 19 // Here we will make some assumptions about the operator. If only 2 values are 20 // passed to the method, we will assume that the operator is an equals sign 21 // and keep going. Otherwise, we'll require the operator to be passed in. 22 list($value, $operator) = $this->prepareValueAndOperator( 23 $value, $operator, func_num_args() == 2 24 ); 25 26 // If the columns is actually a Closure instance, we will assume the developer 27 // wants to begin a nested where statement which is wrapped in parenthesis. 28 // We'll add that Closure to the query then return back out immediately. 29 if ($column instanceof Closure) { 30 return $this->whereNested($column, $boolean); 31 } 32 33 // If the given operator is not found in the list of valid operators we will 34 // assume that the developer is just short-cutting the '=' operators and 35 // we will set the operators to '=' and set the values appropriately. 36 if ($this->invalidOperator($operator)) { 37 list($value, $operator) = [$operator, '=']; 38 } 39 40 // If the value is a Closure, it means the developer is performing an entire 41 // sub-select within the query and we will need to compile the sub-select 42 // within the where clause to get the appropriate query record results. 43 if ($value instanceof Closure) { 44 return $this->whereSub($column, $operator, $value, $boolean); 45 } 46 47 // If the value is "null", we will just assume the developer wants to add a 48 // where null clause to the query. So, we will allow a short-cut here to 49 // that method for convenience so the developer doesn't have to check. 50 if (is_null($value)) { 51 return $this->whereNull($column, $boolean, $operator !== '='); 52 } 53 54 // If the column is making a JSON reference we'll check to see if the value 55 // is a boolean. If it is, we'll add the raw boolean string as an actual 56 // value to the query to ensure this is properly handled by the query. 57 if (Str::contains($column, '->') && is_bool($value)) { 58 $value = new Expression($value ? 'true' : 'false'); 59 } 60 61 // Now that we are working with just a simple query we can put the elements 62 // in our array and add the query binding to our array of bindings that 63 // will be bound to each SQL statements when it is finally executed. 64 $type = 'Basic'; 65 66 $this->wheres[] = compact( 67 'type', 'column', 'operator', 'value', 'boolean' 68 ); 69 70 if (! $value instanceof Expression) { 71 $this->addBinding($value, 'where'); 72 } 73 74 return $this; 75 }
先從這個方法的參數開始,它一共有4個形參,分別表明$column字段、$operator操做符、$value值、$boolean = 'and'。express
從字面意思咱們能夠猜想到,最原始的where方法,一開始是打算像$model->where('age', '>', 18)->get()這樣來進行基本查詢操做的。數組
那麼讓咱們先拋開前面那些if代碼塊,直接跳到方法底部builder類經過compact函數,將基礎參數添加到$this->wheres數組後,在判斷$value不是一個表達式後,跳轉到了addBinding方法中。閉包
1 public function addBinding($value, $type = 'where') 2 { 3 4 if (! array_key_exists($type, $this->bindings)) { 5 throw new InvalidArgumentException("Invalid binding type: {$type}."); 6 } 7 8 if (is_array($value)) { 9 $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); 10 } else { 11 $this->bindings[$type][] = $value; 12 } 13 14 return $this; 15 }
接下來看addBinding方法作了什麼,首先一次array_key_exists校驗肯定傳入條件正確。而後判斷傳入的value是否爲數組,若非數組,則直接將這個值傳入$this->bindings數組的對應操做中。打印出來以下所示。app
隨後便直接返回了$this對象,一個最簡單的where方法就執行完畢了。框架
那麼,按正常操做,接下來就改執行get()方法了。ide
1 public function get($columns = ['*']) 2 { 3 $original = $this->columns; 4 5 if (is_null($original)) { 6 $this->columns = $columns; 7 } 8 9 $results = $this->processor->processSelect($this, $this->runSelect()); 10 11 $this->columns = $original; 12 13 return collect($results); 14 }
這個方法首先獲取了要查詢的字段,若爲空則使用傳入方法的$columns參數。而後經過$this->runSelect()方法進行查詢,經過processor將返回值包裝返回。函數
讓咱們來看一下runSelect()方法,這裏的$this->connection實際上是獲取到pdo的連接對象,select()方法的三個參數分別爲sql語句,pdo爲了防注入將語句與值給分開了,因此第二個參數爲值,第三個參數則是爲了經過參數獲取只讀或讀寫模式的pdo實例。
getBindings()直接從對象中獲取數據,並經過laravel 的 Arr對象進行包裝。
而toSql()方法想要得到sql語句卻沒有那麼簡單,它須要調用多個方法來對sql進行拼接。
protected function runSelect() { return $this->connection->select( $this->toSql(), $this->getBindings(), ! $this->useWritePdo ); } public function getBindings() { return Arr::flatten($this->bindings); } public function toSql() { return $this->grammar->compileSelect($this); }
那麼如今來看一下sql語句是如何獲取到的吧。compileSelect方法位於\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php對象中,它會經過Builder對象中的屬性數據,來拼接一條sql返回出去。
public function compileSelect(Builder $query) { // If the query does not have any columns set, we'll set the columns to the // * character to just get all of the columns from the database. Then we // can build the query and concatenate all the pieces together as one. $original = $query->columns; if (is_null($query->columns)) { $query->columns = ['*']; } // To compile the query, we'll spin through each component of the query and // see if that component exists. If it does we'll just call the compiler // function for the component which is responsible for making the SQL. $sql = trim($this->concatenate( $this->compileComponents($query)) ); $query->columns = $original; return $sql; }
這個方法一開始獲取了語句要查詢的字段。並作了空值判斷,若爲空則查詢 * 。
接下來咱們看一下$this->compileComponents($query)這一句代碼,它的做用是返回基本的sql語句段,返回值以下所示。
而後經過$this->concatenate()方法將其拼接成一條完整的sql語句。爲了搞清楚sql語句是怎麼來的,咱們又得深刻compileComponents方法了。
這個方法位於\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php對象內部。先來看一下它的代碼。
protected function compileComponents(Builder $query) { $sql = []; foreach ($this->selectComponents as $component) { // To compile the query, we'll spin through each component of the query and // see if that component exists. If it does we'll just call the compiler // function for the component which is responsible for making the SQL. if (! is_null($query->$component)) { $method = 'compile'.ucfirst($component); //var_dump($component,$method,$query->$component,'-------'); //將這些條件打印出來看一下 $sql[$component] = $this->$method($query, $query->$component); } } //dd('over'); return $sql; }
這個方法內部,將selectComponents屬性,也就是查詢語句模板,進行了遍歷,並判斷出了,在$query對象中所存在的那一部分。經過這些語句,來構建sql語句片斷。這個模板以下所示。
protected $selectComponents = [ 'aggregate', 'columns', 'from', 'joins', 'wheres', 'groups', 'havings', 'orders', 'limit', 'offset', 'unions', 'lock', ];
而$query對象中所存在的部分,將它們打印後,結果以下所示。經過我上面代碼段中被註釋的部分,將其打印了出來,我在下圖中對三個屬性作了註釋。
總結來說,這個方法會根據builder對象中所存儲的屬性,運行模板方法,將其構建成sql字符串部件。而builder對象中的屬性則是咱們本身經過DB或Model方法添加進去的。
那麼咱們剛剛那句簡單的sql查詢則是運行了compileColumns、compileFrom、compileWheres。這三個方法。
protected function compileColumns(Builder $query, $columns) { // If the query is actually performing an aggregating select, we will let that // compiler handle the building of the select clauses, as it will need some // more syntax that is best handled by that function to keep things neat. if (! is_null($query->aggregate)) { return; } $select = $query->distinct ? 'select distinct ' : 'select '; return $select.$this->columnize($columns); } public function columnize(array $columns) { return implode(', ', array_map([$this, 'wrap'], $columns)); }
先來看compileColumns,這個方法看上去很簡單,判斷aggregate不爲空後,根據distinct 屬性來得出sql語句頭,而後將這個字符串與$this->columnize()方法的返回值進行拼接。就得出了上面'select *'這句字符串。而關鍵在於columnize方法中的array_map的[$this, 'wrap']。
array_map這個函數會傳入兩個參數,第一個參數爲函數名,第二個參數爲數組。將第二個數組參數中的每一個值當成參數,傳入第一個參數所表明的函數中循環執行。
那麼如今咱們要找到wrap這個方法了。
public function wrap($value, $prefixAlias = false) { if ($this->isExpression($value)) { return $this->getValue($value); } // If the value being wrapped has a column alias we will need to separate out // the pieces so we can wrap each of the segments of the expression on it // own, and then joins them both back together with the "as" connector. if (strpos(strtolower($value), ' as ') !== false) { return $this->wrapAliasedValue($value, $prefixAlias); } return $this->wrapSegments(explode('.', $value)); }
這個方法,首先判斷了傳入參數不是一個表達式,而是一個肯定的值。而後strpos(strtolower($value), ' as ') !== false這一句將$value轉爲小寫,並判斷了sql語句中沒有as字段。而後便返回了$this->wrapSegments的值。
protected function wrapSegments($segments) { return collect($segments)->map(function ($segment, $key) use ($segments) { return $key == 0 && count($segments) > 1 ? $this->wrapTable($segment) : $this->wrapValue($segment); })->implode('.'); }
到這裏,咱們會發現這個方法,只是傳入了一個閉包函數,就給返回了,laravel框架實在是難以跟蹤。
事實上collect()方法表明了\vendor\laravel\framework\src\Illuminate\Support\Collection.php對象。
能夠看到在collection類的構造方法中,咱們將參數存入了它的屬性,而在map方法中,經過array_keys對這些屬性作了處理事後,又經過array_map對其進了加工。看下剛剛wrapSegments中的閉包函數是怎麼寫的,他們調用了wrapTable()和wrapValue這兩個方法。根據傳入參數的不一樣,來分別調用。
public function __construct($items = []) { $this->items = $this->getArrayableItems($items); } public function map(callable $callback) { $keys = array_keys($this->items); $items = array_map($callback, $this->items, $keys); return new static(array_combine($keys, $items)); }
protected function wrapValue($value) { if ($value !== '*') { return '"'.str_replace('"', '""', $value).'"'; } return $value; }
若是參數爲*則直接返回了拼接星號的字符串,反之則直接返回了$value數組。而後視線調回collection對象的map方法,返回值在經過array_combine函數加工後,又經過collection本類包裝成了對象返回。到這裏函數調用就到頂了,依次返回值,返回到Grammars對象的compileColumns方法中,與'select'字符串進行拼接後再次返回。這部分sql語句片斷就構建完成了。
那麼接下來就剩compileFrom、compileWheres兩個方法了。
protected function compileFrom(Builder $query, $table) { return 'from '.$this->wrapTable($table); } public function wrapTable($table) { if (! $this->isExpression($table)) { return $this->wrap($this->tablePrefix.$table, true); } return $this->getValue($table); }
from語句的構建比較簡單,直接from接表名就好。可是wrapTable方法中的代碼咱們發現有點眼熟,沒錯,它又調用了wrap方法,還記得咱們剛剛構建select時看到的嗎?這個方法只是對傳入的參數作了解析,幷包裝成集合返回回來。其實不止select和from其餘的語句段構建都要經過wrap方法來進行參數解析。剛剛已經解析過wrap方法,這裏我就很少說了。最後,這個方法也是返回了'from'部分的sql語句片斷。
接下來到compileWheres方法了。
protected function compileWheres(Builder $query) { // Each type of where clauses has its own compiler function which is responsible // for actually creating the where clauses SQL. This helps keep the code nice // and maintainable since each clause has a very small method that it uses. if (is_null($query->wheres)) { return ''; } // If we actually have some where clauses, we will strip off the first boolean // operator, which is added by the query builders for convenience so we can // avoid checking for the first clauses in each of the compilers methods. if (count($sql = $this->compileWheresToArray($query)) > 0) { return $this->concatenateWhereClauses($query, $sql); } return ''; } protected function compileWheresToArray($query) { return collect($query->wheres)->map(function ($where) use ($query) { return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where); })->all(); } protected function concatenateWhereClauses($query, $sql) { $conjunction = $query instanceof JoinClause ? 'on' : 'where'; return $conjunction.' '.$this->removeLeadingBoolean(implode(' ', $sql)); } protected function removeLeadingBoolean($value) { return preg_replace('/and |or /i', '', $value, 1); }
那麼,來看一下。首先compileWheres方法判斷where條件是否爲空,而後compileWheresToArray方法來判斷where參數是否大於0。這個方法用了collect對象的map方法,咱們以前已經看過了。重要的是這個閉包函數,來看一下這個閉包函數幹了什麼。它經過$hwere['type']這個屬性中存儲的字段做爲方法名調用了whereBasic方法,以下所示
protected function whereBasic(Builder $query, $where) { $value = $this->parameter($where['value']); return $this->wrap($where['column']).' '.$where['operator'].' '.$value; }
public function parameter($value) { return $this->isExpression($value) ? $this->getValue($value) : '?'; }
經過parameter方法獲取到參數後,依然是經過wrap包裝參數。concatenateWhereClauses方法根據以前返回的參數,決定拼接'where'字符串,而後經過removeLeadingBoolean方法決定‘and‘等條件的拼接。
到這裏,基礎sql語句片斷就已經所有構建出來了。
視線跳回compileSelect方法的concatenate方法。
protected function concatenate($segments) { return implode(' ', array_filter($segments, function ($value) { return (string) $value !== ''; })); }
經過array_filter與implode函數將sql語句片斷合併爲了一條完整sql語句。
sql語句有了,咱們視線又要跳回Builder對象的runSelect方法了。這個裏面的$this->connection->select()方法對sql進行了調用,返回的即是查詢結果了。connection則是Illuminate\Database\MySqlConnection對象。
protected function runSelect() { return $this->connection->select( $this->toSql(), $this->getBindings(), ! $this->useWritePdo ); }
而select方法則是在它的父類\vendor\laravel\framework\src\Illuminate\Database\Connection.php中。
public function select($query, $bindings = [], $useReadPdo = true) { return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { if ($this->pretending()) { return []; } // For select statements, we'll simply execute the query and return an array // of the database result set. Each element in the array will be a single // row from the database table, and will either be an array or objects. $statement = $this->prepared($this->getPdoForSelect($useReadPdo) ->prepare($query)); $this->bindValues($statement, $this->prepareBindings($bindings)); $statement->execute(); return $statement->fetchAll(); }); } protected function run($query, $bindings, Closure $callback) { $this->reconnectIfMissingConnection(); $start = microtime(true); // Here we will run this query. If an exception occurs we'll determine if it was // caused by a connection that has been lost. If that is the cause, we'll try // to re-establish connection and re-run the query with a fresh connection. try { $result = $this->runQueryCallback($query, $bindings, $callback); } catch (QueryException $e) { $result = $this->handleQueryException( $e, $query, $bindings, $callback ); } // Once we have run the query we will calculate the time that it took to run and // then log the query, bindings, and execution time so we will report them on // the event that the developer needs them. We'll log time in milliseconds. $this->logQuery( $query, $bindings, $this->getElapsedTime($start) ); return $result; } protected function runQueryCallback($query, $bindings, Closure $callback) { // To execute the statement, we'll simply call the callback, which will actually // run the SQL against the PDO connection. Then we can calculate the time it // took to execute and log the query SQL, bindings and time in our memory. try { $result = $callback($query, $bindings); } // If an exception occurs when attempting to run a query, we'll format the error // message to include the bindings with SQL, which will make this exception a // lot more helpful to the developer instead of just the database's errors. catch (Exception $e) { throw new QueryException( $query, $this->prepareBindings($bindings), $e ); } return $result; }
這三個方法,看起來很長一段,可是其中的代碼是很簡單的。咱們一個一個來分析,select方法,只作了一件事,調用run方法,把sql語句,bindings參數,以及一個閉包函數傳入了其中。
而run方法,則是獲取了pdo連接,記錄了開始查詢的毫秒時間,經過runQueryCallback運行了查詢閉包函數,並記錄sql日誌,最後返回了查詢結果。
runQueryCallback方法只是簡單的調用了閉包函數。如今轉回來看閉包函數作了什麼。
$statement = $this->prepared($this->getPdoForSelect($useReadPdo) ->prepare($query)); $this->bindValues($statement, $this->prepareBindings($bindings));
關鍵代碼就是這兩句了。$this->getPdoForSelect($useReadPdo)方法經過以前設置的讀寫方式獲取pdo實例。這裏作了這麼多判斷,最終獲取到的是provider初始化時存入的實例
protected function getPdoForSelect($useReadPdo = true) { return $useReadPdo ? $this->getReadPdo() : $this->getPdo(); } public function getReadPdo() { if ($this->transactions > 0) { return $this->getPdo(); } if ($this->getConfig('sticky') && $this->recordsModified) { return $this->getPdo(); } if ($this->readPdo instanceof Closure) { return $this->readPdo = call_user_func($this->readPdo); } return $this->readPdo ?: $this->getPdo(); }
獲取到pdo對象後,剩下的都是pdo的原生方法了。fetchAll方法返回sql查詢結果集。
而後一直返回到get()方法。
public function get($columns = ['*']) { $original = $this->columns; if (is_null($original)) { $this->columns = $columns; } $results = $this->processor->processSelect($this, $this->runSelect()); $this->columns = $original; return collect($results); }
到這裏,經過collect集合進行包裝以後,便返回到咱們model對象的操做方式了。