簡介
Builder
就是查詢到SQL
轉換的紐帶!
這章很難,勸你放棄閱讀 ?♀️?♂️
老闆和打工仔的故事
`Eloquent Builder` 是我們在使用 `Laravel`
模型進行查詢的時候呼叫的物件,轉換 `SQL` 最終是呼叫了
`Query Builder` 物件的服務。
所以我們將介紹兩個 `Builder` 物件。
複製程式碼
Query Builder
指Illuminate\Database\Query\Builder
Eloquent Builder
指Illuminate\Database\Eloquent\Builder
這兩個物件的關係就像老闆和打工仔,上層Eloquent Builder
指揮下層 Query Builder
幹活。
查詢 User::find(1)
當我們執行這條查詢的時候,會觸發 Model
的方法
這裡不管是否靜態呼叫都沒關係,最終會轉到 __call
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}
複製程式碼
再轉發到
public function __call($method, $parameters)
{
// "如果是這兩個方法的話會優先呼叫 Model自身定義的"
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
// "這裡的 $this->newQuery() 就是 Eloquent Builder 物件!"
// "轉發呼叫,實際執行了 $this->newQuery()->{$method}"
return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}
複製程式碼
首先出場的是
Eloquent Builder
我們來看 $this->newQuery()
獲取的是什麼!
public function newQuery()
{
return $this->registerGlobalScopes($this->newQueryWithoutScopes());
}
複製程式碼
繼續分析 newQueryWithoutScopes()
public function newQueryWithoutScopes()
{
return $this->newModelQuery()
->with($this->with)
->withCount($this->withCount);
}
複製程式碼
public function newModelQuery()
{
return $this->newEloquentBuilder(
$this->newBaseQueryBuilder()
)->setModel($this);
}
複製程式碼
public function newEloquentBuilder($query)
{
return new Builder($query);
}
// "Builder 的構造方法宣告"
public function __construct(QueryBuilder $query)
{
$this->query = $query;
}
複製程式碼
返回一個 Query Builder
物件
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new QueryBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
複製程式碼
其實經過上面一系列的操作最主要的目的就是將 Query Builder
賦值給 Eloquent Builder
可見 Eloquent Builder
並沒有構建 SQL
語句的能力
但是這層封裝使得 Eloquent Builder
擁有了這能力。
所以真正的構建服務還是來自 Query Builder
物件。
經過上面的分析我們回到最開始的呼叫處
public function __call($method, $parameters)
{
// "如果是這兩個方法的話會優先呼叫 Model自身定義的"
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
// "這裡的 $this->newQuery() 就是 Eloquent Builder 物件!"
// "轉發呼叫,實際執行了 $this->newQuery()->{$method}"
return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}
複製程式碼
所以我們對模型層的大部分呼叫都是呼叫 (Eloquent Builder)>{$method}
那麼就從開篇的例子開始分析這個 Eloquent Builder
到底有什麼方法!
Find(1)
方法解析
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
return $this->whereKey($id)->first($columns);
}
複製程式碼
我們傳入的是一個 Int
,直接分析 $this->whereKey($id)->first($columns)
public function whereKey($id)
{
if (is_array($id) || $id instanceof Arrayable) {
$this->query->whereIn($this->model->getQualifiedKeyName(), $id);
return $this;
}
// "從這裡開始分析"
// "$this->model->getQualifiedKeyName() 就是獲取主鍵的名字是什麼,就不贅述"
return $this->where($this->model->getQualifiedKeyName(), '=', $id);
}
複製程式碼
️?繼續看,接下來就是重點了!關於查詢構建器是如何構建 SQL
的。
我們在腦海裡面先想一下,查詢構建器是幹啥的?!
回憶下是不是很久沒有寫原生 SQL
了?還記得 SELECT * FROM users WHERE id = 1;
嗎,在 Laravel
中查詢構建器功能就是將我們的 User::find(1)
轉化成上面的 SQL
好了,我們回來繼續分析如何完成這個轉化!
打工仔現身
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column instanceof Closure) {
$column($query = $this->model->newModelQuery());
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
$this->query->where(...func_get_args());
}
return $this;
}
複製程式碼
執行這裡的程式碼,這裡呼叫了 打工仔 Query Builder
$this->query->where(...func_get_args());
複製程式碼
展開打工仔的 where
, 接收的引數就是上面完完整整的轉發了一次。
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if (is_array($column)) {
return $this->addArrayOfWheres($column, $boolean);
}
[$value, $operator] = $this->prepareValueAndOperator(
$value, $operator, func_num_args() === 2
);
if ($column instanceof Closure) {
return $this->whereNested($column, $boolean);
}
if ($this->invalidOperator($operator)) {
[$value, $operator] = [$operator, '='];
}
if ($value instanceof Closure) {
return $this->whereSub($column, $operator, $value, $boolean);
}
if (is_null($value)) {
return $this->whereNull($column, $boolean, $operator !== '=');
}
if (Str::contains($column, '->') && is_bool($value)) {
$value = new Expression($value ? 'true' : 'false');
}
$type = 'Basic';
$this->wheres[] = compact(
'type', 'column', 'operator', 'value', 'boolean'
);
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}
return $this;
}
複製程式碼
上面這麼一大堆的程式碼實在是懶得講了~看圖吧,
反正就是對 Builder
這幾個圈起來的屬性賦值
仔細看看,反正沒什麼難的,就是先把資料丟到這些成員裡存起來。
上面我們存好了資料,那麼後面我們就要想辦法從這些屬性中構建處 SQL
了,別急,我們現在開始。
執行查詢
回到剛才開始的地方
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
// "剛才執行了這句"
return $this->whereKey($id)->first($columns);
}
複製程式碼
打工仔兄弟 Illuminate\Database\Concerns\BuildsQueries
現身
這裡的 first()
方法是 use
這 BuildsQueries
這個特質類
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
複製程式碼
追進去這裡要注意 $this
這裡指向的是 Eloquent Builder
物件 ,
原始碼裡面是沒有 take
這個方法,這又是通過 __call
方法來呼叫
最終執行程式碼就是 $this->query->take(1)->get($columns)->first()
這裡關於為什麼這樣執行的可以查閱 Eloquent Builder
魔術方法。
接著來
public function take($value)
{
// "就是賦值操作,給物件的 $this->limit = $value;"
return $this->limit($value);
}
複製程式碼
繼續看,準備好秋名山最後幾個關卡來了
// "這個 `get`方法是老闆 `Eloquent Builder` 中定義的"
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
複製程式碼
接著重點是 $builder->getModels($columns)
獲取資料的操作
public function getModels($columns = ['*'])
{
return $this->model->hydrate(
$this->query->get($columns)->all()
)->all();
}
複製程式碼
我們不理會其他,只看 $this->query->get($columns)->all()
這裡就是呼叫打工仔 Query Builder
的 get()
public function get($columns = ['*'])
{
// "onceWithColumns 這個方法沒什麼好分析,接收兩個引數,返回第二個引數(閉包)"
return collect($this->onceWithColumns($columns, function () {
return $this->processor->processSelect($this, $this->runSelect());
}));
}
複製程式碼
繼續看閉包裡面 $this->processor->processSelect($this, $this->runSelect());
public function processSelect(Builder $query, $results)
{
// "這裡沒幹啥,就是把 $results 返回"
return $results;
}
複製程式碼
那麼最重點的來了,看名字就是執行 SQL
$this->runSelect()
複製程式碼
這裡的
$this->connection->select()
是驅動層提供對接MySQL
的呼叫,我們不用關心啦~我們看到這裡的select
有三個引數,第一個就是我們苦苦尋找的SQL
,第二個是PDO
引數繫結的資料。
protected function runSelect()
{
return $this->connection->select(
$this->toSql(), $this->getBindings(), ! $this->useWritePdo
);
}
複製程式碼
toSql()
tips 我們平時在使用
(new User)->getQuery()->toSql();
可以看到預編譯的SQL
public function toSql()
{
return $this->grammar->compileSelect($this);
}
...
// "方便閱讀合併了部分原始碼"
public function compileSelect(Builder $query)
{
if ($query->unions && $query->aggregate) {
$column = $this->columnize($aggregate['columns']);
if ($query->distinct && $column !== '*') {
$column = 'distinct '.$column;
}
$sql = 'select '.$aggregate['function'].'('.$column.') as aggregate';
$query->aggregate = null;
$sql = $sql.' from ('.$this->compileSelect($query).') as '.$this->wrapTable('temp_table');
}
$original = $query->columns;
if (is_null($query->columns)) {
$query->columns = ['*'];
}
$sql = trim($this->concatenate(
$this->compileComponents($query))
);
$query->columns = $original;
return $sql;
}
複製程式碼
這一坨坨代實在講起來沒有味道,就是各種判斷,然後抽取屬性拼接成字串。
這裡面有興趣可以自行研究,這篇僅僅介紹執行邏輯。
總結
老闆
Eloquent Builder
和打工仔Query Builder
的職責!
Builder
的原理,先存入屬性,在執行toSql()
其他功能等待讀者開發!