本文分析的Model类位于\Think\Model.class.php。Model类在框架中起着至关重要的作用。开发者建立的Model模型最终都会继承基础Model类。
也会涉及thinkphp中一些常用的函数,我也会进行相应的分析。基础Model类中 涉及:
本文不再贴出Model源码,想查看源码请前往官网下载。
/** * 自动检测数据表信息 * @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(); } }
/** * 获取字段信息并缓存 * @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基础类通过这两个函数实现了字段的缓存功能。
$M->where('id=84')->order("id desc")->select();这样的查询表达式在thinkphp框架中非常实现,但是又找不到order、where对应的方法,它们是如何实现的呢?
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; } }
/** * 新增数据 * @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()函数进行字段类型检查和强制类型转换。这就是为什么我们在插入数据时,可以忽略数据的字段类型。暖暖的,很贴心,有木有。最后通过设置的过滤函数处理数据。如
/** * 数据类型检测 * @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();
/** * 字段值增长 * @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秒后统一更新积分到数据库,而不是每次都更新数据库。临时积分会被累积并缓存起来,最后到了延迟更新时间,再统一更新。