CodeIgniter源码分析 7.2 - 数据库驱动之查询构造器

查询构造器

什么是查询构造器

查询构造器是建立在sql语句上的抽象,其本身是一些已经封装好的方法,使用时只需要传入参数,其内部封装的逻辑会将参数解析成sql语句,进而与数据库交互。

查询构造器的意义

查询构造器的意义在于能够使你使用较少的代码来实现数据的读,写,更新,并且易于维护的代码;同时还能避免一定程度上的 sql 注入。

现在让我们用查询构造器写下这么一段代码获取数据

$query = $this->db
          ->select('user_id,user_phone')
          ->from('users')
          ->where(['user_id > ' => 1, 'is_lock' => 0])
          ->limit(0, 10)
          ->order_by('user_id desc')
          ->get();

然后我们看下查询构造器是如何解析这段查询的

select()

select() 中传递是查询字段,那我们就很好奇传进去的字段是怎么被处理的,有没有被转义之类的?

public function select($select = '*', $escape = NULL)
    {
        //将字段处理成数组
        if (is_string($select))
        {
            $select = explode(',', $select);
        }

        //第二个参数控制器是否对参数字段进行转义,如果没有改参数,那么使用默认的处理方式
        is_bool($escape) OR $escape = $this->_protect_identifiers;

        // 将查询字段存储到特定的数组中,并同时记录它的转义方式以及是否对字段缓存,
        // 字段缓存会用在查询构造器缓存和缓存重置相应的逻辑处
        foreach ($select as $val)
        {
            $val = trim($val);

            if ($val !== '')
            {
                $this->qb_select[] = $val;
                $this->qb_no_escape[] = $escape;

                if ($this->qb_caching === TRUE)
                {
                    $this->qb_cache_select[] = $val;
                    $this->qb_cache_exists[] = 'select';
                    $this->qb_cache_no_escape[] = $escape;
                }
            }
        }

        return $this;
    }

通过分析 select() 的源码,我们发现查询字段竟然是保存在特定的数组中,有木有似曾相识的感觉?

如果大家研究过模板引擎的话,发现该处处理查询字段的方式和模板引擎的编译很像,类似模板引擎编译成抽象语法树的方式来解析查询构造器参数,这种方式不得不说好聪明;不然拼 sql ,还要做好安全性的话,代码会变成一团乱麻!

那么就可以大胆的假设了,查询构造器其他函数的处理方式会不会也类似 select () 呢?

where()

我们知道查询构造器提供给了我们4种传递查询条件的方式

简单的 key=>value: $this->db->where('name', $name);
含有运算符的 key=>value : $this->db->where('name !=', $name);
关联数组: $this->db->where(['name' => $name);
自定义字符串: $this->db->where("name=$name");

那 where() 函数对这四种情况又是怎么处理的呢?

public function where($key, $value = NULL, $escape = NULL)
    {
        return $this->_wh('qb_where', $key, $value, 'AND ', $escape);
    }

可以看到对where的参数还是解析到一个特定数组中去了,所以很肯定了,其他方法也是着这种类似抽象语法树的方式解析参数的

注意:后两种相对前两种没有第二个参数,对于和where,or_where 等相关的条件查询都是在 _wh() 这个方法中进行的

protected function _wh($qb_key, $key, $value = NULL, $type = 'AND ', $escape = NULL)
    {

        //根据 db_key 决定是 hava的缓存key还是where的缓存key,该key一样会用在查询构造器缓存相关的逻辑;
       //由于where和having的查询语法很相似,所以你会发现having()方法也调用了_wh()!
        $qb_cache_key = ($qb_key === 'qb_having') ? 'qb_cache_having' : 'qb_cache_where';

        //将那四种查询方式都处理成关联数组的方式,那么很肯定对于最后一种,他的value是null
        if ( ! is_array($key))
        {
            $key = array($key => $value);
        }

        //对查询条件进行转义相关的设置
        is_bool($escape) OR $escape = $this->_protect_identifiers;

        foreach ($key as $k => $v)
        {
            // $prefix 是连接查询条件的关键字,它是由 $type 得来的,也就是and,or之类的关键字
            $prefix = (count($this->$qb_key) === 0 && count($this->$qb_cache_key) === 0)
                ? $this->_group_get_type('')
                : $this->_group_get_type($type);

            //首先判断 value 是不是为null,也就是where 查询条件的前两种方式,
           //对于没有运算符的 $key 需要接一个等号(说明是简单的key=>value条件查询)
            if ($v !== NULL)
            {
                if ($escape === TRUE)
                {
                    $v = ' '.$this->escape($v);
                }

                if ( ! $this->_has_operator($k))
                {
                    $k .= ' = ';
                }
            }
            // 如果$k没有运算符并且value为空,那就是说明可能使用了类似 $this->db->where('name') 这种没有条件值的查询条件,
            // 为了避免sql错误,接一个 IS NULL
            elseif ( ! $this->_has_operator($k))
            {
                // value appears not to have been set, assign the test to IS NULL
                $k .= ' IS NULL';
            }
            elseif (preg_match('/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE))
            {
                $k = substr($k, 0, $match[0][1]).($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL');
            }

            //最后将条件值拼起来,扔到qb_where下,后期解析查询条件时只需要将其中的条件拼接起来就行了,同时将查询条件也缓存一下
            $this->{$qb_key}[] = array('condition' => $prefix.$k.$v, 'escape' => $escape);
            if ($this->qb_caching === TRUE)
            {
                $this->{$qb_cache_key}[] = array('condition' => $prefix.$k.$v, 'escape' => $escape);
                $this->qb_cache_exists[] = substr($qb_key, 3);
            }

        }
        
        //为了支持链式调用,返回当前对象
        return $this;
    }

from()

public function from($from)
    {
        /*
         * from 就是要查询的表名了,可以看到from是支持多表名传入的,不过这种情况不多见;
         *  _track_aliases() 是处理表别名的函数,其内部通过判断是否有 AS 关键字会解析到表别名,
         * 然后将别名存储到 qb_aliased_tables 这个数组下!
         *
         * 那么你可能会有疑问,设置的表别名后,那之前查询字段怎么办?如果是关联查询,没有命名空间的
         * 字段一定会引起的歧义的;其实你看到的 _protect_identifiers() 这个函数就是处理这种情况的!
         * 并且如果你的表名是 host.dbname.table table_alias 这种情况的话,该函数也能处理!
         * 
         * 
         * 最后解析到表名后在写到相关的映射数组中去,并缓存
         * */
        foreach ((array) $from as $val)
        {
            if (strpos($val, ',') !== FALSE)
            {
                foreach (explode(',', $val) as $v)
                {
                    $v = trim($v);
                    $this->_track_aliases($v);

                    $this->qb_from[] = $v = $this->protect_identifiers($v, TRUE, NULL, FALSE);

                    if ($this->qb_caching === TRUE)
                    {
                        $this->qb_cache_from[] = $v;
                        $this->qb_cache_exists[] = 'from';
                    }
                }
            }
            else
            {
                $val = trim($val);

                // Extract any aliases that might exist. We use this information
                // in the protect_identifiers to know whether to add a table prefix
                $this->_track_aliases($val);

                $this->qb_from[] = $val = $this->protect_identifiers($val, TRUE, NULL, FALSE);

                if ($this->qb_caching === TRUE)
                {
                    $this->qb_cache_from[] = $val;
                    $this->qb_cache_exists[] = 'from';
                }
            }
        }

        return $this;
    }

limit()

limit 就很简单了,由于 limt 参数不像 where 那样有多组,所以 limit 的 两个参数是直接扔在变量上的!

public function limit($value, $offset = 0)
{
    is_null($value) OR $this->qb_limit = (int) $value;
    empty($offset) OR $this->qb_offset = (int) $offset;
    return $this;
}

order_by()

order_by 是处理排序的,我们知道其要两种入参方式:

简单的 key => value : $this->db->order_by('id', 'DESC');
字符串: $this->db->order_by('id DESC, ctime DESC');

看下 order_by 对这两种排序的处理

public function order_by($orderby, $direction = '', $escape = NULL)
{
        // 将排序关键字转成大写 desc => DESC,asc => ASC
        $direction = strtoupper(trim($direction));

        // 如果排序关键字是RANDOM,说明是随机排序
        if ($direction === 'RANDOM')
        {
            $direction = '';

            // Do we have a seed value?
            $orderby = ctype_digit((string) $orderby)
                ? sprintf($this->_random_keyword[1], $orderby)
                : $this->_random_keyword[0];

        }
        elseif (empty($orderby))
        {
            return $this;
        }
        // 处理排序关键字,对于随机排序 $direction 是空的
        elseif ($direction !== '')
        {
            $direction = in_array($direction, array('ASC', 'DESC'), TRUE) ? ' '.$direction : '';
        }

        is_bool($escape) OR $escape = $this->_protect_identifiers;

        if ($escape === FALSE)
        {
            $qb_orderby[] = array('field' => $orderby, 'direction' => $direction, 'escape' => FALSE);
        }
        else
        {
            /*
             * 这里就是处理解析排序参数的核心处了,首先不管是 key => valude 风格还是字符串风格,都处理成数组的方式,
             * 接下来根据 $direction 判断是该排序是正常的字段排序还是随机排序
             * */

            $qb_orderby = array();
            foreach (explode(',', $orderby) as $field)
            {
                $qb_orderby[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE))
                    ? array('field' => ltrim(substr($field, 0, $match[0][1])), 'direction' => ' '.$match[1][0], 'escape' => TRUE)
                    : array('field' => trim($field), 'direction' => $direction, 'escape' => TRUE);
            }
        }
        
        //由于我们可能会多次调用 $this->order_by,这势必会导致qb_orderby不为空,每调一次,就需要添加到之前的排序数组中去
        $this->qb_orderby = array_merge($this->qb_orderby, $qb_orderby);
        if ($this->qb_caching === TRUE)
        {
            $this->qb_cache_orderby = array_merge($this->qb_cache_orderby, $qb_orderby);
            $this->qb_cache_exists[] = 'orderby';
        }

        return $this;
}

get()

当所有查询构造器参数被解析完毕后,对于 get() 来说就是将这些参数拼成sql,进而获取查询结果了!

public function get($table = '', $limit = NULL, $offset = NULL)
{
        //如果你没有使用 from 设置要查询的表,那么你还可以通过 get 传入表名,
        // 传入表名后需要解析别名,也看到get其实调用了 from 设置了要查询的表
        if ($table !== '')
        {
            $this->_track_aliases($table);
            $this->from($table);
        }
        
        if ( ! empty($limit))
        {
            $this->limit($limit, $offset);
        }
        
        //看到没,最终的获取查询的结果的方式是通过在 _compile_select 中拼成 sql 然后传给了 query
        $result = $this->query($this->_compile_select());
        $this->_reset_select();
        return $result;
 }

关于查询构造器的核心就是 _compile_select() 这个函数了,在这个函数中我们会看到将查询构造器的参数解析成了 sql。

protected function _compile_select($select_override = FALSE)
    {
        // 将没有缓存的查询构造器参数在缓存中存一份,如果开启查询构造器缓存,其实下面的 qb_xxx 就是 
       //db_cache_xxx ,因为 _merge_cache 内部中将 db_cache_xxx 赋给了 qb_xxx,这个不难理解,有缓存当然是先从缓存中读数据了 
        $this->_merge_cache();

        // $select_override 是from前面的部分
        if ($select_override !== FALSE)
        {
            $sql = $select_override;
        }
        else
        {
            
            $sql = ( ! $this->qb_distinct) ? 'SELECT ' : 'SELECT DISTINCT ';

            if (count($this->qb_select) === 0)
            {
                $sql .= '*';
            }
            else
            {
                /*
                 * 该部分就是对查询字段的拼接了,可以看到其对字段做了是否转义的设置,
                 * 而 protect_identifiers 则进行字段转义,是否为给字段加别名的处理
                 * 
                 * */
                
                foreach ($this->qb_select as $key => $val)
                {
                    $no_escape = isset($this->qb_no_escape[$key]) ? $this->qb_no_escape[$key] : NULL;
                    $this->qb_select[$key] = $this->protect_identifiers($val, FALSE, $no_escape);
                }

                $sql .= implode(', ', $this->qb_select);
            }
        }

        // 拼接表名,由于from可以支持传入多个表,那么这里 _from_tables 其实就是 implode 传入
        // 的多个表名而已;一般情况下from 传入多表名的情况很少
        if (count($this->qb_from) > 0)
        {
            $sql .= "\nFROM ".$this->_from_tables();
        }

        // 拼接处理join
        if (count($this->qb_join) > 0)
        {
            $sql .= "\n".implode("\n", $this->qb_join);
        }
        
        // 拼接查询条件,分组,排序等
        $sql .= $this->_compile_wh('qb_where')
            .$this->_compile_group_by()
            .$this->_compile_wh('qb_having')
            .$this->_compile_order_by(); // ORDER BY

        // LIMIT
        if ($this->qb_limit)
        {
            return $this->_limit($sql."\n");
        }
        
        //最后将拼成的sql返回
        return $sql;
    }

几个重要的查询构造器函数的源码就分析到这里了,下节看下事务处理相关的源码!

你可能感兴趣的:(CodeIgniter源码分析 7.2 - 数据库驱动之查询构造器)