频道栏目
首页 > 资讯 > ThinkPHP > 正文

ThinkPHP框架Model分析研究

16-10-17        来源:[db:作者]  
收藏   我要投稿

本文分析的Model类位于\Think\Model.class.php。Model类在框架中起着至关重要的作用。开发者建立的Model模型最终都会继承基础Model类。

也会涉及thinkphp中一些常用的函数,我也会进行相应的分析。基础Model类中 涉及:

1、查询数据表中字段的缓存功能
2、方法的重载去实现thinkphp常用的查询函数
3、命名范围的使用及好处
4、添加数据前对数据的处理及字段类型验证
5、延迟更新数据等

本文不再贴出Model源码,想查看源码请前往官网下载。

1、查询数据表中字段的缓存功能

/**
 * 自动检测数据表信息
 * @access protected
 * @return void
 */
protected function _checkTableInfo() {
    // 如果不是Model类 自动记录数据表信息
    // 只在第一次执行记录
    if(empty($this->fields)) {
        // 如果数据表字段没有定义则自动获取
        if(C('DB_FIELDS_CACHE')) {
            $db   =  $this->dbName?:C('DB_NAME');
            $fields = F('_fields/'.strtolower($db.'.'.$this->tablePrefix.$this->name));
            if($fields) {
                $this->fields   =   $fields;
                if(!empty($fields['_pk'])){
                    $this->pk       =   $fields['_pk'];
                }
                return ;
            }
        }
        // 每次都会读取数据表信息
        $this->flush();
    }
}
DB_FIELDS_CACHE 在框架中默认是关闭的 ,即是C("DB_FIELDS_CACHE")返回false的,可以在相应的应用配置文件中设置这个选项。开启以后,查询表时,先检测是否有缓存字段,如果有就使用缓存的字段,不再去进行数据库的读取,节省了数据库资源。如果没有,才进行数据库的读取,并且通过 $this->flush() 函数将表的字段名称及类型缓存到如下文件夹下:
 
缓存字段函数如下:
/**
 * 获取字段信息并缓存
 * @access public
 * @return void
 */
public function flush() {
    // 缓存不存在则查询数据表信息
    $this->db->setModel($this->name);
    $fields =   $this->db->getFields($this->getTableName());
    if(!$fields) { // 无法获取字段信息
        return false;
    }
    $this->fields   =   array_keys($fields);
    unset($this->fields['_pk']);
    foreach ($fields as $key=>$val){
        // 记录字段类型
        $type[$key]     =   $val['type'];
        if($val['primary']) {
              // 增加复合主键支持
            if (isset($this->fields['_pk']) && $this->fields['_pk'] != null) {
                if (is_string($this->fields['_pk'])) {
                    $this->pk   =   array($this->fields['_pk']);
                    $this->fields['_pk']   =   $this->pk;
                }
                $this->pk[]   =   $key;
                $this->fields['_pk'][]   =   $key;
            } else {
                $this->pk   =   $key;
                $this->fields['_pk']   =   $key;
            }
            if($val['autoinc']) $this->autoinc   =   true;
        }
    }
    // 记录字段类型信息
    $this->fields['_type'] =  $type;

    // 2008-3-7 增加缓存开关控制
    if(C('DB_FIELDS_CACHE')){
        // 永久缓存数据表信息
        $db   =  $this->dbName?:C('DB_NAME');
        F('_fields/'.strtolower($db.'.'.$this->tablePrefix.$this->name),$this->fields);
    }
}
两个函数同时用到了缓存函数F函数。F函数快速文件数据读取和保存 针对简单类型数据 字符串、数组,下文我们还会提到另一个缓存函数S,它可以支持复查的数据类型及缓存有效期的设置等。总之Model基础类通过这两个函数实现了字段的缓存功能。

2、方法的重载去实现thinkphp常用的查询函数

$M->where('id=84')->order("id desc")->select();
这样的查询表达式在thinkphp框架中非常实现,但是又找不到order、where对应的方法,它们是如何实现的呢?
这样的情况下,一般都是方法的重载实现的。Model类也是如此。
public function __call($method,$args) {
    if(in_array(strtolower($method),$this->methods,true)) {
        // 连贯操作的实现
        $this->options[strtolower($method)] =   $args[0];
        return $this;
    }elseif(in_array(strtolower($method),array('count','sum','min','max','avg'),true)){
        // 统计查询的实现
        $field =  isset($args[0])?$args[0]:'*';
        return $this->getField(strtoupper($method).'('.$field.') AS tp_'.$method);
    }elseif(strtolower(substr($method,0,5))=='getby') {
        // 根据某个字段获取记录
        $field   =   parse_name(substr($method,5));
        $where[$field] =  $args[0];
        return $this->where($where)->find();
    }elseif(strtolower(substr($method,0,10))=='getfieldby') {
        // 根据某个字段获取记录的某个值
        $name   =   parse_name(substr($method,10));
        $where[$name] =$args[0];
        return $this->where($where)->getField($args[1]);
    }elseif(isset($this->_scope[$method])){// 命名范围的单独调用支持
        return $this->scope($method,$args[0]);
    }else{
        E(__CLASS__.':'.$method.L('_METHOD_NOT_EXIST_'));
        return;
    }
}
至于实现了哪些方法,同志们不妨可以看看这段代码,也就一目了然了。当然$this->scope()函数涉及到下面的命名范围

3、命名范围的使用及好处

命名范围函数简单就不贴了。主要说一下命名范围的好处:命名范围功能的优势在于可以一次定义多次调用,并且在项目中也能起到分工配合的规范。至于如何调用请看官网。

4、添加数据前对数据的处理及字段类型验证

/**
 * 新增数据
 * @access public
 * @param mixed $data 数据
 * @param array $options 表达式
 * @param boolean $replace 是否replace
 * @return mixed
 */
public function add($data='',$options=array(),$replace=false) {
    if(empty($data)) {
        // 没有传递数据,获取当前数据对象的值
        if(!empty($this->data)) {
            $data           =   $this->data;
            // 重置数据
            $this->data     = array();
        }else{
            $this->error    = L('_DATA_TYPE_INVALID_');
            return false;
        }
    }
    // 数据处理
    $data       =   $this->_facade($data);
    // 分析表达式
    $options    =   $this->_parseOptions($options);
    if(false === $this->_before_insert($data,$options)) {
        return false;
    }
    // 写入数据到数据库
    $result = $this->db->insert($data,$options,$replace);
    if(false !== $result && is_numeric($result)) {
        $pk     =   $this->getPk();
          // 增加复合主键支持
        if (is_array($pk)) return $result;
        $insertId   =   $this->getLastInsID();
        if($insertId) {
            // 自增主键返回插入ID
            $data[$pk]  = $insertId;
            if(false === $this->_after_insert($data,$options)) {
                return false;
            }
            return $insertId;
        }
        if(false === $this->_after_insert($data,$options)) {
            return false;
        }
    }
    return $result;
}
// 数据处理
$data       =   $this->_facade($data);
// 分析表达式
$options    =   $this->_parseOptions($options);

这两步是插入数据库之前的重要的操作。我们先看_facade() 函数

/**
 * 对保存到数据库的数据进行处理
 * @access protected
 * @param mixed $data 要操作的数据
 * @return boolean
 */
 protected function _facade($data) {

    // 检查数据字段合法性
    if(!empty($this->fields)) {
        if(!empty($this->options['field'])) {
            $fields =   $this->options['field'];
            unset($this->options['field']);
            if(is_string($fields)) {
                $fields =   explode(',',$fields);
            }    
        }else{
            $fields =   $this->fields;
        }        
        foreach ($data as $key=>$val){
            if(!in_array($key,$fields,true)){
                if(!empty($this->options['strict'])){
                    E(L('_DATA_TYPE_INVALID_').':['.$key.'=>'.$val.']');
                }
                unset($data[$key]);
            }elseif(is_scalar($val)) {
                // 字段类型检查 和 强制转换
                $this->_parseType($data,$key);
            }
        }
    }
   
    // 安全过滤
    if(!empty($this->options['filter'])) {
        $data = array_map($this->options['filter'],$data);
        unset($this->options['filter']);
    }
    $this->_before_write($data);
    return $data;
 }
可以看到这个函数先获取相应表格操作的字段信息,然后通过_parseType()函数进行字段类型检查和强制类型转换。这就是为什么我们在插入数据时,可以忽略数据的字段类型。暖暖的,很贴心,有木有。最后通过设置的过滤函数处理数据。如
  1. $Model->filter('strip_tags')->add(); 其实是通过 上面$data=array_map($this->options["filter"],$data); 如果不懂array_map 请百度
    /**
     * 数据类型检测
     * @access protected
     * @param mixed $data 数据
     * @param string $key 字段名
     * @return void
     */
    protected function _parseType(&$data,$key) {
        if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
            $fieldType = strtolower($this->fields['_type'][$key]);
            if(false !== strpos($fieldType,'enum')){
                // 支持ENUM类型优先检测
            }elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
                $data[$key]   =  intval($data[$key]);
            }elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
                $data[$key]   =  floatval($data[$key]);
            }elseif(false !== strpos($fieldType,'bool')){
                $data[$key]   =  (bool)$data[$key];
            }
        }
    }
    接着讲add()函数。还有一个_parseOptions函数。
    /**
     * 分析表达式
     * @access protected
     * @param array $options 表达式参数
     * @return array
     */
    protected function _parseOptions($options=array()) {
        if(is_array($options))
            $options =  array_merge($this->options,$options);
    
        if(!isset($options['table'])){
            // 自动获取表名
            $options['table']   =   $this->getTableName();
            $fields             =   $this->fields;
        }else{
            // 指定数据表 则重新获取字段列表 但不支持类型检测
            $fields             =   $this->getDbFields();
        }
    
        // 数据表别名
        if(!empty($options['alias'])) {
            $options['table']  .=   ' '.$options['alias'];
        }
        // 记录操作的模型名称
        $options['model']       =   $this->name;
    
        // 字段类型验证
        if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
            // 对数组查询条件进行字段类型检查
            foreach ($options['where'] as $key=>$val){
                $key            =   trim($key);
                if(in_array($key,$fields,true)){
                    if(is_scalar($val)) {
                        $this->_parseType($options['where'],$key);
                    }
                }elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){
                    if(!empty($this->options['strict'])){
                        E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');
                    } 
                    unset($options['where'][$key]);
                }
            }
        }
        // 查询过后清空sql表达式组装 避免影响下次查询
        $this->options  =   array();
        // 表达式过滤
        $this->_options_filter($options);
        return $options;
    }
    这个函数的作用主要在查询时防止sql注入。由上面的代码知道只有满足if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']

    这个条件才会进行字段类型检查和强制类型转换。 由于在 进行select查询时,也用到这个函数,所以在进行条件查询时,查询条件必须以数组的形式书写,才能防止sql注入。

    $where["status"]=1;
    $where["name"]="wodl";
    $M->where($where)->order("id desc")->select();
    而不能这样书写:
    $M->where("status=1 and name=wodl")->order("id desc")->select();

    5、延迟更新数据

    我们经常需要给某些数据表添加一些需要经常更新的统计字段,例如用户的积分、文件的下载次数等等,而当这些数据更新的频率比较频繁的时候,数据库的压力也随之增大不少,我们可以利用thinkphp的setInc 和setDec 函数实现延迟更新。
    /**
     * 字段值增长
     * @access public
     * @param string $field  字段名
     * @param integer $step  增长值
     * @param integer $lazyTime  延时时间(s)
     * @return boolean
     */
    public function setInc($field,$step=1,$lazyTime=0) {
        if($lazyTime>0) {// 延迟写入
            $condition     =      $this->options['where'];
            $guid     =      md5($this->name.'_'.$field.'_'.serialize($condition));
            $step     =  $this->lazyWrite($guid,$step,$lazyTime);
            if(empty($step)) {
               return true; // 等待下次写入
            }elseif($step < 0) {
               $step  =  '-'.$step;
            }
        }
        return $this->setField($field,array('exp',$field.'+'.$step));
    }
    /**
     * 延时更新检查 返回false表示需要延时
     * 否则返回实际写入的数值
     * @access public
     * @param string $guid  写入标识
     * @param integer $step  写入步进值
     * @param integer $lazyTime  延时时间(s)
     * @return false|integer
     */
    protected function lazyWrite($guid,$step,$lazyTime) {
        if(false !== ($value = S($guid))) { // 存在缓存写入数据
            if(NOW_TIME > S($guid.'_time')+$lazyTime) {
                // 延时更新时间到了,删除缓存数据 并实际写入数据库
                S($guid,NULL);
                S($guid.'_time',NULL);
                return $value+$step;
            }else{
                // 追加数据到缓存
                S($guid,$value+$step);
                return false;
            }
        }else{ // 没有缓存数据
            S($guid,$step);
            // 计时开始
            S($guid.'_time',NOW_TIME);
            return false;
        }
    }
    如果不用延迟更新的话,每执行一次都要往数据库里面更新下字段,流量大的话,数据库都受不了,所以可以使用thinkphp下面的操作
    $M->where("id=4")->setInc("score",4,60);

    那么60秒内执行的所有积分更新操作都会被延迟,实际会在60秒后统一更新积分到数据库,而不是每次都更新数据库。临时积分会被累积并缓存起来,最后到了延迟更新时间,再统一更新。

相关TAG标签
上一篇:MFC相关知识
下一篇:suricata 3.1 源码分析29:数据包队列
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站