ThinkPHP 框架Model 分析研究
本文分析的Model类位于ThinkModel.class.php。Model类在框架中起着至关重要的作用。开发者建立的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()函数进行字段类型检查和强制类型转换。这就是为什么我们在插入数据时,可以忽略数据的字段类型。暖暖的,很贴心,有木有。最后通过设置的过滤函数处理数据。如
- $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秒后统一更新积分到数据库,而不是每次都更新数据库。临时积分会被累积并缓存起来,最后到了延迟更新时间,再统一更新。
花了两个小时写完了,如果对你有用,请关注并顶一下。
声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。
- 上一篇: mysql 特殊字符存取
- 下一篇:没有了
