laravel5.5源码笔记(八、Eloquent ORM)
上一篇写到eloquent orm的基类builder类,这次就来看一下这些方便的orm方法是如何转换成sql语句运行的。
首先还是进入\vendor\laravel\framework\src\illuminate\database\query\builder.php这个类中,先来看一下最常用的where()方法。
如下所示,where方法的代码很长,但前面多个if都是用来兼容各种不同调用式的。我们先抛开这些花哨的调用方式,来看一下最简单的调用方法是怎么运行的。
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'。
从字面意思我们可以猜测到,最原始的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数组的对应操作中。打印出来如下所示。
随后便直接返回了$this对象,一个最简单的where方法就执行完毕了。
那么,按正常操作,接下来就改执行get()方法了。
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对象的操作方式了。
下一篇: 沈括的一生都是什么样的?有何伟大贡献?