美文网首页MySQL
ORM学习(四) - 查询构造器

ORM学习(四) - 查询构造器

作者: 半亩房顶 | 来源:发表于2020-01-28 16:54 被阅读0次

    前言

    上一节学习了CRUD 操作源码。下面几节来看下将代码优雅组织起来的查询构造器吧
    本文主要内容整理自leoyang的系列文章,详情见引用

    查询构造器

    DB::table 与 查询构造器

    在深入开展查询构造器的开始阶段,我们先通过一条简单的语句来说明下查询构造器的执行过程
    DB::table('table')->get();
    首先,这个DB就是我们之前说的connection,然后是table函数

        // \Illuminate\Database\Connection::table
        public function table($table, $as = null)
        {
            return $this->query()->from($table, $as);
        }
    

    这个链式操作的第一步table函数

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

    然后是query(),这个函数返回了一个Builder(Query/Builder),有三个参数,自身、语法编译器、结果处理器

        public function from($table)
        {
            $this->from = $table;
    
            return $this;
        }
    

    跟在query后面的from在这里比较简单,其他的我们后面会提到,这里只是设定了表明。设定完成之后,正式移交到Builder,开始执行get()

        public function get($columns = ['*'])
        {
            return collect($this->onceWithColumns(Arr::wrap($columns), function () {
                return $this->processor->processSelect($this, $this->runSelect());
            }));
        }
    

    直接使用结果处理函数,嵌套runSelect()

        public function processSelect(Builder $query, $results)
        {
            return $results;
        }
    
        protected function runSelect()
        {
            return $this->connection->select(
                $this->toSql(), $this->getBindings(), ! $this->useWritePdo
            );
        }
    

    runSelect真正的调起connection,发起sql调用,toSql()获取sql语句,getBindings()获取绑定参数
    直到toSql(),真正使用语法编译器,生成sql语句。grammar是懒加载的,其中的编译等方法,都是在toSql() 时才会调用。

        public function toSql()
        {
            return $this->grammar->compileSelect($this);
        }
    

    下面开始,详细的看看每个步骤吧

    语法编译器

    先来说下语法编译器,相对于connection的CRUD,语法编译器中都有相对应的函数执行编译操作
    compileSelectcompileInsertcompileDeletecompileUpdate等等。
    其中尤其要注意的是compileSelect函数,因为这里面包含了太多的功能,因为你会发现很多compile函数并没有调用,当然不是没有使用,而是被包含在了compileComponents函数中了,如下代码所示:

    protected $selectComponents = [
            'aggregate',
            'columns',
            'from',
            'joins',
            'wheres',
            'groups',
            'havings',
            'orders',
            'limit',
            'offset',
            'lock',
        ];
    
        /**
         * Compile a select query into SQL.
         *
         * @param  \Illuminate\Database\Query\Builder  $query
         * @return string
         */
        public function compileSelect(Builder $query)
        {
            if ($query->unions && $query->aggregate) {
                return $this->compileUnionAggregate($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))
            );
    
            if ($query->unions) {
                $sql = $this->wrapUnion($sql).' '.$this->compileUnions($query);
            }
    
            $query->columns = $original;
    
            return $sql;
        }
    
        /**
         * Compile the components necessary for a select clause.
         *
         * @param  \Illuminate\Database\Query\Builder  $query
         * @return array
         */
        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 (isset($query->$component) && ! is_null($query->$component)) {
                    $method = 'compile'.ucfirst($component);
    
                    $sql[$component] = $this->$method($query, $query->$component);
                }
            }
    
            return $sql;
        }
    

    语法编译器会将上述所有的语句放入 $sql[] 成员中,然后通过 concatenate 函数组装成 sql 语句:

    protected function concatenate($segments)
    {
        return implode(' ', array_filter($segments, function ($value) {
            return (string) $value !== '';
        }));
    }
    

    然后,我们还有一系列函数不得不提,wrap函数,这些是grammer中的函数,作用是处理表名、变量名等等,wrap函数本身是用来处理表名和列名的,处理流程如下:

    • 若是 Expression 对象,利用函数 getValue 直接取出对象值,不对其进行任何处理,用于处理原生 sql。expression 对象的作用是保护原始参数,避免框架解析的一种方式。也就是说,当我们用了 expression 来包装参数的话,laravel 将不会对其进行任何处理,包括库名解析、表名前缀、别名等。
    • 若表名 / 列名存在 as,则利用函数 wrapAliasedValue 为表名设置别名。
    • 若表名 / 列名含有 .,则会被分解为 库名/表名,或者 表名/列名,并调用函数 wrapSegments。

    代码在此:

    public function wrap($value, $prefixAlias = false)
    {
        if ($this->isExpression($value)) {
            return $this->getValue($value);
        }
    
        if (strpos(strtolower($value), ' as ') !== false) {
            return $this->wrapAliasedValue($value, $prefixAlias);
        }
    
        return $this->wrapSegments(explode('.', $value));
    }
    

    其他还有 wrapAliasedValue,wrapSegments,wrapValue等等,各有各的功能,感兴趣可以去源码看下。

    from 语句

    下面来说下刚才看到了的from语句,上面我们知道了,执行DB::table()时实际上是执行了from函数,它完成了表名的设定,代码再放一遍:

        // \Illuminate\Database\Connection::table
        public function table($table, $as = null)
        {
            return $this->query()->from($table, $as);
        }
    

    这个本身木有什么了,但是我们主要想说的是grammar 中对应的compileFrom 函数:

    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 时,可以传递两种参数,一种是字符串,另一种是 expression 对象,示例:

    DB::table('table');
    DB::table(new Expression('table'));
    
    • 传递 expression 对象,什么是expression?(我的理解是有点像是一个SQL代码写的存储好的变量的对象,嗯)

    当我们传递 expression 对象的时候,grammer 就会调用 getValue 取出原生 sql 语句。

    • 传递字符串
      当我们向 from 传递普通的字符串时,laravel 就会对字符串调用 wrap 函数进行处理,处理流程上一个小节已经说明:
    • 为表名加上前缀 $this->tablePrefix
    • 若字符串存在 as,则为表名设置别名。
    • 若字符串含有 .,则会被分解为 库名表名,并进行分别调用 wrapTable 函数与 wrapValue 进行处理。
    • 为表名前后添加 ",例如 t1.t2 会被转化为 "t1"."t2"(不同的数据库添加的字符不同,mysql 就不是 "

    这里放出本文原作者发现的一个问题,大家感兴趣的可以看下:laravelfrom 处理流程存在一些问题,表名前缀设置功能与数据库名功能公用存在问题,相关 issue 地址是:[Bug] Table prefix added to database name when using database.table

    select 语句

    好的,接下来,从select语句开始

    public function select($columns = ['*'])
    {
        $this->columns = is_array($columns) ? $columns : func_get_args();
    
        return $this;
    }
    
    public function selectRaw($expression, array $bindings = [])
    {
        $this->addSelect(new Expression($expression));
    
        if ($bindings) {
            $this->addBinding($bindings, 'select');
        }
    
        return $this;
    }
    
    public function addSelect($column)
    {
        $column = is_array($column) ? $column : func_get_args();
    
        $this->columns = array_merge((array) $this->columns, $column);
    
        return $this;
    }
    

    selectRaw 是传入expression对象时的处理
    select 语句在SQL语句中,其实就是列的选择,也就是from之前的部分。
    grammar中对应的处理函数就是compileColumns 函数:

    protected function compileColumns(Builder $query, $columns)
    {
        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));
    }
    

    select 函数中还会有一种比较特殊的,selectSub。laravel 的 selectSub 支持闭包函数、queryBuild 对象或者原生 sql 语句,以下是单元测试样例:

    $query = DB::table('one')->select(['foo', 'bar'])->where('key', '=', 'val');
    
    $query->selectSub(function ($query) {
            $query->from('two')->select('baz')->where('subkey', '=', 'subval');
        }, 'sub');
    

    另一种写法:

    $query = DB::table('one')->select(['foo', 'bar'])->where('key', '=', 'val');
    $query_sub = DB::table('one')->select('baz')->where('subkey', '=', 'subval');
    
    $query->selectSub($query_sub, 'sub');
    

    生成的 sql:

    select "foo", "bar", (select "baz" from "two" where "subkey" = 'subval') as "sub" from "one" where "key" = 'val'
    

    selectSub 语句的实现其实并不复杂:

    public function selectSub($query, $as)
    {
        if ($query instanceof Closure) {
            $callback = $query;
    
            $callback($query = $this->forSubQuery());
        }
    
        list($query, $bindings) = $this->parseSubSelect($query);
    
        return $this->selectRaw(
            '('.$query.') as '.$this->grammar->wrap($as), $bindings
        );
    }
    
    protected function parseSubSelect($query)
    {
        if ($query instanceof self) {
            $query->columns = [$query->columns[0]];
    
            return [$query->toSql(), $query->getBindings()];
        } elseif (is_string($query)) {
            return [$query, []];
        } else {
            throw new InvalidArgumentException;
        }
    }
    

    可以看到,如果 selectSub 的参数是闭包函数,那么就会先执行闭包函数,闭包函数将会为 query 根据查询语句更新对象。parseSubSelect 函数为子查询解析 sql 语句与 binding 变量。

    where 部分语句

    from语句前面已经说过了,下面来看where语句,先糊一脸源代码

    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (is_array($column)) {
            return $this->addArrayOfWheres($column, $boolean);
        }
    
        list($value, $operator) = $this->prepareValueAndOperator(
            $value, $operator, func_num_args() == 2
        );
    
        if ($column instanceof Closure) {
            return $this->whereNested($column, $boolean);
        }
    
        if ($this->invalidOperator($operator)) {
            list($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;
    }
    

    满眼望去一堆堆的条件语句,我们挑选其中最常见的两个,结合grammar代码来看:

    where 语句

    首先是对wheres的处理,compileWheres 函数负责所有 where 查询条件的语法编译工作,compileWheresToArray 函数负责循环编译查询条件,concatenateWhereClauses 函数负责将多个查询条件合并。
    compileWheresToArray 函数负责把 $query->wheres 中多个 where 条件循环起来:

    • $where['boolean'] 是多个查询条件的连接,and 或者 or,一般 where 条件默认为 and, 各种 orWhere 的连接是 or
    • where{$where['type']} 是查询的类型,laravel 把查询条件分为以下几类:base、raw、in、notIn、inSub、notInSub、null、notNull、between、column、nested、sub、exist、notExist。每种类型的查询条件都有对应的 grammer 方法

    concatenateWhereClauses 函数负责连接所有的搜索条件,由于 join 的连接条件也会调用 compileWheres 函数,所以会有判断是否是真正的 where 查询

    protected function compileWheres(Builder $query)
    {
        if (is_null($query->wheres)) {
            return '';
        }
    
        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));
    }
    
    where 数组

    如果column是数组,就会调用:

    protected function addArrayOfWheres($column, $boolean, $method = 'where')
    {
        return $this->whereNested(function ($query) use ($column, $method, $boolean) {
            foreach ($column as $key => $value) {
                if (is_numeric($key) && is_array($value)) {
                    $query->{$method}(...array_values($value));
                } else {
                    $query->$method($key, '=', $value, $boolean);
                }
            }
        }, $boolean);
    }
    

    可以看到,数组分为两类,一种是列名为 key,例如 ['foo' => 1, 'bar' => 2],这个时候就是调用 query->where('foo', '=', '1', ‘and’)。还有一种是 [['foo','1'],['bar','2']],这个时候就会调用 $query->where(['foo','1'])

    public function whereNested(Closure $callback, $boolean = 'and')
    {
        call_user_func($callback, $query = $this->forNestedWhere());
    
        return $this->addNestedWhereQuery($query, $boolean);
    }
    
    public function addNestedWhereQuery($query, $boolean = 'and')
    {
        if (count($query->wheres)) {
            $type = 'Nested';
    
            $this->wheres[] = compact('type', 'query', 'boolean');
    
            $this->addBinding($query->getBindings(), 'where');
        }
    
        return $this;
    }
    

    由于 compileWheres 会返回 where ... 或者 on ... 等开头的 sql 语句,所以我们需要把返回结果截取前 3 个字符或 6 个字符。

    其他的更多where语句分析请看原文

    join 语句

    join 语句对数据库进行连接操作,join 函数的连接条件可以非常简单:

    DB::table('services')->select('*')->join('translations AS t', 't.item_id', '=', 'services.id');
    

    也可以比较复杂:

    DB::table('users')->select('*')->join('contacts', function ($j) {
            $j->on('users.id', '=', 'contacts.id')->orOn('users.name', '=', 'contacts.name');
        });
        //select * from "users" inner join "contacts" on "users"."id" = "contacts"."id" or "users"."name" = "contacts"."name"
    
        $builder = $this->getBuilder();
        DB::table('users')->select('*')->from('users')->joinWhere('contacts', 'col1', function ($j) {
            $j->select('users.col2')->from('users')->where('users.id', '=', 'foo')
        });
        //select * from "users" inner join "contacts" on "col1" = (select "users"."col2" from "users" where "users"."id" = foo)
    

    还可以更加复杂!

    DB::table('users')->select('*')->leftJoin('contacts', function ($j) {
            $j->on('users.id', '=', 'contacts.id')->where(function ($j) {
                $j->where('contacts.country', '=', 'US')->orWhere('contacts.is_partner', '=', 1);
            });
        });
        //select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and ("contacts"."country" = 'US' or "contacts"."is_partner" = 1)
    
        DB::table('users')->select('*')->leftJoin('contacts', function ($j) {
            $j->on('users.id', '=', 'contacts.id')->where('contacts.is_active', '=', 1)->orOn(function ($j) {
                $j->orWhere(function ($j) {
                    $j->where('contacts.country', '=', 'UK')->orOn('contacts.type', '=', 'users.type');
                })->where(function ($j) {
                    $j->where('contacts.country', '=', 'US')->orWhereNull('contacts.is_partner');
                });
            });
        });
        //select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and "contacts"."is_active" = 1 or (("contacts"."country" = 'UK' or "contacts"."type" = "users"."type") and ("contacts"."country" = 'US' or "contacts"."is_partner" is null))
    

    其实 join 语句与 where 语句非常相似,将 join 语句的连接条件看作 where 的查询条件完全可以,接下来我们看看源码。

    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
    {
        $join = new JoinClause($this, $type, $table);
    
        if ($first instanceof Closure) {
            call_user_func($first, $join);
    
            $this->joins[] = $join;
    
            $this->addBinding($join->getBindings(), 'join');
        }
    
        else {
            $method = $where ? 'where' : 'on';
    
            $this->joins[] = $join->$method($first, $operator, $second);
    
            $this->addBinding($join->getBindings(), 'join');
        }
    
        return $this;
    }
    

    可以看到,程序首先新建了一个 JoinClause 类对象,这个类实际上继承 queryBuilder,也就是说 queryBuilder 上的很多方法它都可以直接用,例如 where、whereNull、whereDate 等等。
    如果第二个参数是闭包函数的话,就会像查询组一样根据查询条件更新 $join

    如果第二个参数是列名,那么就会调用 on 方法或 where 方法。这两个方法的区别是,on 方法只支持 whereColumn 方法和 whereNested,也就是说只能写出 join on col1 = col2 这样的语句,而 where 方法可以传递数组、子查询等等.

    public function on($first, $operator = null, $second = null, $boolean = 'and')
    {
        if ($first instanceof Closure) {
            return $this->whereNested($first, $boolean);
        }
    
        return $this->whereColumn($first, $operator, $second, $boolean);
    }
    
    public function orOn($first, $operator = null, $second = null)
    {
        return $this->on($first, $operator, $second, 'or');
    }
    

    grammer——compileJoins
    接下来我们来看看如何编译 join 语句:

    protected function compileJoins(Builder $query, $joins)
    {
        return collect($joins)->map(function ($join) use ($query) {
            $table = $this->wrapTable($join->table);
    
            return trim("{$join->type} join {$table} {$this->compileWheres($join)}");
        })->implode(' ');
    }
    

    可以看到,JoinClause 在编译中是作为 queryBuild 对象来看待的。

    union 语句

    union 用于合并两个或多个 SELECT 语句的结果集。Union 因为要进行重复值扫描,所以效率低。如果合并没有刻意要删除重复行,那么就使用 Union All。

    我们在 laravel 中可以这样使用:

    $query = DB::table('users')->select('*')->where('id', '=', 1);
    $query->union(DB::table('users')->select('*')->where('id', '=', 2));
    //(select * from `users` where `id` = 1) union (select * from `users` where `id` = 2)
    

    还可以添加多个 union 语句:

    $query = DB::table('users')->select('*')->where('id', '=', 1);
    $query->union(DB::table('users')->select('*')->where('id', '=', 2));
    $query->union(DB::table('users')->select('*')->where('id', '=', 3));      
    //(select * from "users" where "id" = 1) union (select * from "users" where "id" = 2) union (select * from "users" where "id" = 3)
    

    union 语句可以与 orderBy 相结合:

    $query = DB::table('users')->select('*')->where('id', '=', 1);
    $query->union(DB::table('users')->select('*')->where('id', '=', 2));
    $query->orderBy('id', 'desc');
    //(select * from `users` where `id` = ?) union (select * from `users` where `id` = ?) order by `id` desc
    

    union 语句可以与 limit、offset 相结合:

    $query = DB::table('users')->select('*');
    $query->union(DB::table('users')->select('*'));
    $builder->skip(5)->take(10);
    //(select * from `users`) union (select * from `dogs`) limit 10 offset 5
    

    union 函数
    union 函数比较简单:

    public function union($query, $all = false)
    {
        if ($query instanceof Closure) {
            call_user_func($query, $query = $this->newQuery());
        }
    
        $this->unions[] = compact('query', 'all');
    
        $this->addBinding($query->getBindings(), 'union');
    
        return $this;
    }
    

    语法编译器对 union 的处理:

    public function compileSelect(Builder $query)
    {
        $sql = parent::compileSelect($query);
    
        if ($query->unions) {
            $sql = '('.$sql.') '.$this->compileUnions($query);
        }
    
        return $sql;
    }
    
    protected function compileUnions(Builder $query)
    {
        $sql = '';
    
        foreach ($query->unions as $union) {
            $sql .= $this->compileUnion($union);
        }
    
        if (! empty($query->unionOrders)) {
            $sql .= ' '.$this->compileOrders($query, $query->unionOrders);
        }
    
        if (isset($query->unionLimit)) {
            $sql .= ' '.$this->compileLimit($query, $query->unionLimit);
        }
    
        if (isset($query->unionOffset)) {
            $sql .= ' '.$this->compileOffset($query, $query->unionOffset);
        }
    
        return ltrim($sql);
    }
    
    protected function compileUnion(array $union)
    {
        $conjuction = $union['all'] ? ' union all ' : ' union ';
    
        return $conjuction.'('.$union['query']->toSql().')';
    }
    

    可以看出,union 的处理比较简单,都是调用 query->toSql 语句而已。值得注意的是,在处理 union 的时候,要特别处理 order、limit、offset。

    orderBy 语句

    orderBy 语句用法很简单,可以设置多个排序字段,也可以用原生排序语句:

    DB::table('users')->select('*')->orderBy('email')->orderBy('age', 'desc');
    
    DB::table('users')->select('*')->orderBy('email')->orderByRaw('age desc');
    

    如果当前查询中有 union 的话,排序的变量会被放入 unionOrders 数组中,这个数组只有在 compileUnions 函数中才会被编译成 sql 语句。否则会被放入 orders 数组中,这时会被 compileOrders 处理:

    public function orderBy($column, $direction = 'asc')
    {
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
            'column' => $column,
            'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
        ];
    
        return $this;
    }
    

    grammar中orderBy 的编译也很简单:

    protected function compileOrders(Builder $query, $orders)
    {
        if (! empty($orders)) {
            return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders));
        }
    
        return '';
    }
    
    protected function compileOrdersToArray(Builder $query, $orders)
    {
        return array_map(function ($order) {
            return ! isset($order['sql'])
                        ? $this->wrap($order['column']).' '.$order['direction']
                        : $order['sql'];
        }, $orders);
    }
    

    group 语句

    groupBy 语句的参数形式有多种:

    DB::select('*')->from('users')->groupBy('email');
    
    DB::select('*')->from('users')->groupBy('id', 'email');
    
    DB::select('*')->from('users')->groupBy(['id', 'email']);
    
    DB::select('*')->from('users')->groupBy(new Raw('DATE(created_at)'));
    

    groupBy 函数很简单,仅仅是为 $this->groups 成员变量合并数组:

    public function groupBy(...$groups)
    {
        foreach ($groups as $group) {
            $this->groups = array_merge(
                (array) $this->groups,
                Arr::wrap($group)
            );
        }
    
        return $this;
    }
    

    语法编译器的处理:

    protected function compileGroups(Builder $query, $groups)
    {
        return 'group by '.$this->columnize($groups);
    }
    

    insert 语句

    insert 语句也是我们经常使用的数据库操作,它的源码如下:

    public function insert(array $values)
    {
        if (empty($values)) {
            return true;
        }
    
        if (! is_array(reset($values))) {
            $values = [$values];
        }
        else {
            foreach ($values as $key => $value) {
                ksort($value);
    
                $values[$key] = $value;
            }
        }
    
        return $this->connection->insert(
            $this->grammar->compileInsert($this, $values),
            $this->cleanBindings(Arr::flatten($values, 1))
        );
    }
    

    laravel 的 insert 是允许批量插入的,方法如下:

    DB::table('users')->insert([['email' => 'foo', 'name' => 'taylor'], ['email' => 'bar', 'name' => 'dayle']]);
    

    一个语句可以向数据库插入两条记录。sql 语句为:

    insert into users (email,name) values ('foo', 'taylor'), ('bar', 'dayle');
    因此,laravel 在处理 insert 的时候,首先会判断当前的参数是单条插入还是批量插入。

    if (! is_array(reset($values))) {
        $values = [$values];
    }
    

    reset 会返回 values 的第一个元素。如果是批量插入的话,第一个元素必然也是数组。如果的单条插入的话,第一个元素是列名与列值。因此如果是单条插入的话,会在最外层再套一个数组,统一插入的格式。

    如果是批量插入的话,首先需要把插入的各个字段进行排序,保证插入时各个记录的列顺序一致。

    grammar中compileInsert对 insert 的编译也是按照批量插入的标准来进行的:

    public function compileInsert(Builder $query, array $values)
    {
        $table = $this->wrapTable($query->from);
    
        if (! is_array(reset($values))) {
            $values = [$values];
        }
    
        $columns = $this->columnize(array_keys(reset($values)));
    
        $parameters = collect($values)->map(function ($record) {
            return '('.$this->parameterize($record).')';
        })->implode(', ');
    
        return "insert into $table ($columns) values $parameters";
    }
    

    首先对插入的列名进行 columnze 函数处理,之后对每个记录的插入都调用 parameterize 函数来对列值进行处理,并用 () 包围起来。

    update 语句

    public function update(array $values)
    {
        $sql = $this->grammar->compileUpdate($this, $values);
    
        return $this->connection->update($sql, $this->cleanBindings(
            $this->grammar->prepareBindingsForUpdate($this->bindings, $values)
        ));
    }
    

    与插入语句相比,更新语句更加复杂,因为更新语句必然带有 where 条件,有时还会有 join 条件:

    public function compileUpdate(Builder $query, $values)
    {
        $table = $this->wrapTable($query->from);
    
        $columns = collect($values)->map(function ($value, $key) {
            return $this->wrap($key).' = '.$this->parameter($value);
        })->implode(', ');
    
        $joins = '';
    
        if (isset($query->joins)) {
            $joins = ' '.$this->compileJoins($query, $query->joins);
        }
    
        $wheres = $this->compileWheres($query);
    
        return trim("update {$table}{$joins} set $columns $wheres");
    }
    

    其中有一个我们也比较常用的功能,updateOrInsert,其语句会先根据 attributes 条件查询,如果查询失败,就会合并 attributes 与 values 两个数组,并插入新的记录。如果查询成功,就会利用 values 更新数据。

    public function updateOrInsert(array $attributes, array $values = [])
    {
        if (! $this->where($attributes)->exists()) {
            return $this->insert(array_merge($attributes, $values));
        }
    
        return (bool) $this->take(1)->update($values);
    }
    

    delete 语句

    删除语句比较简单,参数仅仅需要 id 即可,delete 语句会添加 id 的 where 条件:

    public function delete($id = null)
    {
        if (! is_null($id)) {
            $this->where($this->from.'.id', '=', $id);
        }
    
        return $this->connection->delete(
            $this->grammar->compileDelete($this), $this->getBindings()
        );
    }
    

    删除语句的编译需要先编译 where 条件:

    public function compileDelete(Builder $query)
    {
        $wheres = is_array($query->wheres) ? $this->compileWheres($query) : '';
    
        return trim("delete from {$this->wrapTable($query->from)} $wheres");
    }
    

    动态 where

    laravel 有一个有趣的功能:动态 where。
    DB::table('users')->whereFooBarAndBazOrQux('corge', 'waldo', 'fred')
    这个语句会生成下面的 sql 语句:
    select * from users where foo_bar = 'corge' and baz = 'waldo' or qux = 'fred';
    也就是说,动态 where 将函数名解析为列名与连接条件,将参数作为搜索的值。

    我们先看源码:

    public function dynamicWhere($method, $parameters)
    {
    
        $finder = substr($method, 5);
    
        $segments = preg_split(
            '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
        );
    
        $connector = 'and';
    
        $index = 0;
    
        foreach ($segments as $segment) {
            if ($segment !== 'And' && $segment !== 'Or') {
                $this->addDynamic($segment, $connector, $parameters, $index);
    
                $index++;
            }
    
            else {
                $connector = $segment;
            }
        }
    
        return $this;
    }
    
    protected function addDynamic($segment, $connector, $parameters, $index)
    {
        $bool = strtolower($connector);
    
        $this->where(Str::snake($segment), '=', $parameters[$index], $bool);
    }
    
    • 首先,程序会提取函数名 whereFooBarAndBazOrQux,删除前 5 个字符得到FooBarAndBazOrQux
    • 然后正则判断,根据 And 或 Or 对函数名进行切割:FooBar、And、Baz、Or、Qux
    • 添加 where 条件,将驼峰命名改为蛇型命名。

    总结

    其实原文的查询构造器是分为三篇的,但是我这里只提出了常用的一些语句,然后汇总成了一篇,当然会丢失很多语句的处理,不过也能给出一个全貌,这也算是我个人的一个习惯吧。下一篇开始Eloquent Model 源码分析。

    引用

    Laravel 官方文档 -- 查询构造器
    Laravel Database——查询构造器与语法编译器源码分析 (上)
    Laravel Database——查询构造器与语法编译器源码分析 (中)
    Laravel Database——查询构造器与语法编译器源码分析 (下)
    Database 查询构建器
    如何写一个属于自己的数据库封装(6) - 查询 - WHERE篇

    以上

    欢迎大家关注我的公众号


    半亩房顶

    相关文章

      网友评论

        本文标题:ORM学习(四) - 查询构造器

        本文链接:https://www.haomeiwen.com/subject/zytiactx.html