Skip to content

查询构造器

当使用Eloquent时,我们通常会使用QueryBuilder构造器来构建查询,调用语句如下。

php
Route::get('/', function (){
    \Illuminate\Support\Facades\DB::table('users')->get();
});

当我们调用table方法时,实际使用的是Connection对应实现的类的方法,以MysqlConnection为例,在该类中未找到table方法,该方法在父类中。

php
use Illuminate\Database\Query\Builder as QueryBuilder;
public function table($table, $as = null)
{
    return $this->query()->from($table, $as);
}

public function query()
{
    return new QueryBuilder(
        $this, $this->getQueryGrammar(), $this->getPostProcessor()
    );
}

public function getQueryGrammar()
{
    return $this->queryGrammar;
}
public function getPostProcessor()
{
    return $this->postProcessor;
}
public function setPostProcessor(Processor $processor)
{
    $this->postProcessor = $processor;

    return $this;
}
public function setQueryGrammar(Query\Grammars\Grammar $grammar)
{
    $this->queryGrammar = $grammar;

    return $this;
}

上面可以看出,query方法返回的是Builder实例,getQueryGrammar方法返回的是Query\Grammars\Grammar实例,getPostProcessor方法返回的是Processor实例,setPostProcessor方法设置Processor实例,setQueryGrammar方法设置Query\Grammars\Grammar实例。

查看Builder类,

php
public function __construct(ConnectionInterface $connection,
                            Grammar $grammar = null,
                            Processor $processor = null)
{
    $this->connection = $connection;
    $this->grammar = $grammar ?: $connection->getQueryGrammar();
    $this->processor = $processor ?: $connection->getPostProcessor();
}

之后,由Builder类调用from方法,

php
public function from($table, $as = null)
{
    // 如果 $table 是一个查询构建器实例,则调用 fromSub 方法
    if ($this->isQueryable($table)) {
        return $this->fromSub($table, $as);
    }
    // 设置表名,如果传入别名则设置别名
    $this->from = $as ? "{$table} as {$as}" : $table;

    return $this;
}
// ......
protected function isQueryable($value)
    {
        return $value instanceof self ||
               $value instanceof EloquentBuilder ||
               $value instanceof Relation ||
               $value instanceof Closure;
    }

前期的准备工作已经完成,下面开始分析where方法。

Builder类

分析一下where方法,当调用DB::table('users')->where('id', 1)时,查看Builder类的where方法,

php
// Illuminate\Database\Query\Builder;

/**
 * Add a basic where clause to the query.
 * Parameters:
 * @var array|Closure|string $column 可以为数组、字符串、闭包
 * @var mixed $operator
 * @var mixed $value 
 * @var string $boolean
 * @return Builder
 */
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
    // 处理数组,最终通过递归的方式,将数组转化为单个字段
    if (is_array($column)) {
        return $this->addArrayOfWheres($column, $boolean);
    }
    /**
     *   protected function addArrayOfWheres($column, $boolean, $method = 'where')
     *   {
     *       return $this->whereNested(function ($query) use ($column, $method, $boolean) {
     *           foreach ($column as $key => $value) {
     *               如果是数组,则递归调用 where 方法
     *               if (is_numeric($key) && is_array($value)) {
     *                   $query->{$method}(...array_values($value));
     *               } else {
     *                   $query->$method($key, '=', $value, $boolean);
     *               }
     *           }
     *       }, $boolean);
     *   }
     */
    // 处理操作符
    [$value, $operator] = $this->prepareValueAndOperator(
        $value, $operator, func_num_args() === 2
    );
    /**
     * public function prepareValueAndOperator($value, $operator, $useDefault = false)
     * {
     *       如果传入参数等于2,则追加一个等于符号
     *       if ($useDefault) {
     *           return [$operator, '='];
     *       对$operator的值进行校验
     *       } elseif ($this->invalidOperatorAndValue($operator, $value)) {
     *           throw new InvalidArgumentException('Illegal operator and value combination.');
     *       }
     *   return [$value, $operator];
     * }
     */

    // 处理闭包
    if ($column instanceof Closure && is_null($operator)) {
        return $this->whereNested($column, $boolean);
    }

    // 处理可查询对象
    if ($this->isQueryable($column) && ! is_null($operator)) {
        [$sub, $bindings] = $this->createSub($column);

        return $this->addBinding($bindings, 'where')
            ->where(new Expression('('.$sub.')'), $operator, $value, $boolean);
    }

    // 处理运算符,如果$operator无效
    if ($this->invalidOperator($operator)) {
        [$value, $operator] = [$operator, '='];
    }

    // 处理value值是闭包的情况
    if ($value instanceof Closure) {
        return $this->whereSub($column, $operator, $value, $boolean);
    }

    // 如果value为空,则追加is null的条件
    if (is_null($value)) {
        return $this->whereNull($column, $boolean, $operator !== '=');
    }

    $type = 'Basic';

    // 处理JSON引用
    if (Str::contains($column, '->') && is_bool($value)) {
        $value = new Expression($value ? 'true' : 'false');

        if (is_string($column)) {
            $type = 'JsonBoolean';
        }
    }

    if ($this->isBitwiseOperator($operator)) {
        $type = 'Bitwise';
    }

    // 将所有条件放在wheres数组中
    $this->wheres[] = compact(
        'type', 'column', 'operator', 'value', 'boolean'
    );
    // 最后将查询条件添加到该查询构造器的bingings数组中
    if (! $value instanceof Expression) {
        $this->addBinding($this->flattenValue($value), 'where');
    }

    return $this;
}

实操

$column为字符串时,最简单的一种情况

web.php中添加一个路由闭包

php
Route::get('/', function (){
    \Illuminate\Support\Facades\DB::table('users')->where('id', '1')->get();
});

使用xdebug,观察调用过程。打开Illuminate\Database\Query\Builder.php文件,找到where方法。 将这一条语句存放到$this->wheres数组中,

php
$this->wheres[] = compact(
            'type', 'column', 'operator', 'value', 'boolean'
        );

// 如果value不是Expression实例
// 传过来的value为1
if (! $value instanceof Expression) {
    $this->addBinding($this->flattenValue($value), 'where');
}
/**
 * protected function flattenValue($value)
 * {
 *     // head(Arr::flatten($value))获取多维数组 $value 展平后的一维数组的第一个元素
 *     return is_array($value) ? head(Arr::flatten($value)) : $value;
 * }
 */
// 直接进入addBinding方法
public function addBinding($value, $type = 'where')
{
    if (! array_key_exists($type, $this->bindings)) {
        throw new InvalidArgumentException("Invalid binding type: {$type}.");
    }
    // 如果value是数组
    if (is_array($value)) {
        $this->bindings[$type] = array_values(array_map(
            [$this, 'castBinding'],
            array_merge($this->bindings[$type], $value),
        ));
    } else {
        // 走这条,将value值存放到bindings数组中
        $this->bindings[$type][] = $this->castBinding($value);
    }

    return $this;
}
// 处理绑定,这里$value是1,直接返回
public function castBinding($value)
{
    if (function_exists('enum_exists') && $value instanceof BackedEnum) {
        return $value->value;
    }

    return $value;
}

上面流程走完,where方法执行结束,where方法后调用dd()方法。

php
Route::get('/', function (){
    \Illuminate\Support\Facades\DB::table('users')->where(['id' => '1'])->dd();
});
结果

刷新首页,dd()方法打印当前构建的 SQL 查询语句和绑定的参数,(dump()方法同样)

$column为数组时

php
Route::get('/', function (){
    \Illuminate\Support\Facades\DB::table('users')->where(['id' => '1'])->dd();
});

会进入到addArrayOfWheres方法中,可以看到,返回调用whereNested方法之后的内容,

php
protected function addArrayOfWheres($column, $boolean, $method = 'where')
{
    // 首先调用whereNested方法,第一个参数为闭包,
    // 第二个参数为where方法中最后一个参数,默认为and
    return $this->whereNested(function ($query) use ($column, $method, $boolean) {
        // 数组为['id' => '1']
        foreach ($column as $key => $value) {
            if (is_numeric($key) && is_array($value)) {
                $query->{$method}(...array_values($value));
            } else {
                // 走else,相当于递归调用where方法
                $query->$method($key, '=', $value, $boolean);
            }
        }
    }, $boolean);
}

public function whereNested(Closure $callback, $boolean = 'and')
{
    // 第一个参数为闭包,
    // call_user_func函数,把第一个参数当作回调函数,其余为参数
    // 反过来执行上面闭包中的内容
    // $this->forNestedWhere() 该方法创建一个新的查询构造器
    // 在laravel中用于嵌套查询
    call_user_func($callback, $query = $this->forNestedWhere());

    return $this->addNestedWhereQuery($query, $boolean);
}
// public function forNestedWhere()
// {
//     return $this->newQuery()->from($this->from);
// }

// public function newQuery()
// {
//     return new static($this->connection, $this->grammar, $this->processor);
// }

提示

$this->forNestedWhere() 该方法创建一个新的查询构造器builder类实例

将断点打到这行$query->$method($key, '=', $value, $boolean);,查看debug内容 点击下一步,再次进入where方法,此时参数为 此时,和{%mark color:red $column为字符串时 %} 情况相同,$this->wheres数组中,添加该条件,然后进入addBinding方法,将$value存放到$this->bindings数组中。

结果

查看dd()方法的输出, 可以发现跟$column为字符串时结果一样。

当$column为闭包且操作符为null时

where方法中对应的是以下代码,

php
if ($column instanceof Closure && is_null($operator)) {
            return $this->whereNested($column, $boolean);
        }

更改路由,

php
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

Route::get('/', function (){
    DB::table('users')->where(function (Builder $query) {
        return $query->where('id',1);
    })->dd();
});

debug如下,

php
public function whereNested(Closure $callback, $boolean = 'and')
{
    call_user_func($callback, $query = $this->forNestedWhere());

    return $this->addNestedWhereQuery($query, $boolean);
}

该方法已经分析过,会执行闭包,参数为新的查询构造器。重点看第二行

php
return $this->addNestedWhereQuery($query, $boolean);

该方法将新创建的查询构造器和$boolean参数传入到首次实例化的查询构造器中。

php
public function addNestedWhereQuery($query, $boolean = 'and')
{
    // 如果查询构造器中有where条件
    if (count($query->wheres)) {
        // 类型为嵌套查询
        $type = 'Nested';
        // 会将参数绑定到第一个查询构造器中
        $this->wheres[] = compact('type', 'query', 'boolean');
        // $query->getRawBindings()['where']该方法会返回新查询构造器中保存的where条件
        // addBinding同理,将新查询构造器中的where条件添加到当前查询构造器的bingding变量中
        $this->addBinding($query->getRawBindings()['where'], 'where');
    }

    return $this;
}

debug信息可知,闭包中的条件在新的查询构造器$query变量中,需要从新查询构造器中取出,添加到当前查询构造器中。 而添加的方法就是addNestedWhereQuery方法。

结果

可以发现转成sql语句后,结果和$column为字符串时一样。但是通过闭包形式可以创建更复杂的查询,代价就是效率会变低。

当$column为闭包且操作符不为null时

该方法在where方法中对应的是以下代码,这里可以构建更加复杂的子查询。

php
if ($this->isQueryable($column) && ! is_null($operator)) {
    [$sub, $bindings] = $this->createSub($column);

    return $this->addBinding($bindings, 'where')
        ->where(new Expression('('.$sub.')'), $operator, $value, $boolean);
}

protected function isQueryable($value)
{
    return $value instanceof self ||
           $value instanceof EloquentBuilder ||
           $value instanceof Relation ||
           $value instanceof Closure;
}

如果值为闭包时

该方法在where方法中对应的是以下代码,

php
if ($value instanceof Closure) {
    return $this->whereSub($column, $operator, $value, $boolean);
}

有兴趣可自行研究。