laravel5.5源碼筆記(8、Eloquent ORM)

上一篇寫到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     }
View Code

 

先從這個方法的參數開始,它一共有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對象的操做方式了。

相關文章
相關標籤/搜索