阅读 223

【Laravel-海贼王系列】第十六章,Builder 解析

简介

Builder 就是查询到 SQL 转换的纽带!

这章很难,劝你放弃阅读 🤷‍♀️🤷‍♂️

老板和打工仔的故事

 `Eloquent Builder` 是我们在使用 `Laravel` 
 模型进行查询的时候调用的对象,转换 `SQL` 最终是调用了
 `Query Builder` 对象的服务。
 所以我们将介绍两个 `Builder` 对象。
复制代码

Query BuilderIlluminate\Database\Query\Builder

Eloquent BuilderIlluminate\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() 方法是 useBuildsQueries 这个特质类

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 Builderget()

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()

其他功能等待读者开发!

关注下面的标签,发现更多相似文章
评论