6.22 高级模型

高级模型提供了更多的查询功能和模型增强功能,利用了模型类的扩展机制实现。如果需要使用高级模型的下面这些功能,记得需要继承AdvModel类或者采用动态模型。class UserModel extends AdvModel{
}
我们下面的示例都假设UserModel类继承自AdvModel类。

字段过滤

基础模型类内置有数据自动完成功能,可以对字段进行过滤,但是必须通过Create方法调用才能生效。高级模型类的字段过滤功能却可以不受create方法的调用限制,可以在模型里面定义各个字段的过滤机制,包括写入过滤和读取过滤。
字段过滤的设置方式只需要在Model类里面添加 $_filter属性,并且加入过滤因子,格式如下:protected $_filter = array(
    '过滤的字段'=>array('写入过滤规则','读取过滤规则',是否传入整个数据对象),
)
过滤的规则是一个函数,如果设置传入整个数据对象,那么函数的参数就是整个数据对象,默认是传入数据对象中该字段的值。
举例说明,例如我们需要在发表文章的时候对文章内容进行安全过滤,并且希望在读取的时候进行截取前面255个字符,那么可以设置:protected $_filter = array(
    'content'=>array('contentWriteFilter','contentReadFilter'),
)
其中,contentWriteFilter是自定义的对字符串进行安全过滤的函数,而contentReadFilter是自定义的一个对内容进行截取的函数。通常我们可以在项目的公共函数文件里面定义这些函数。

序列化字段

序列化字段是新版推出的新功能,可以用简单的数据表字段完成复杂的表单数据存储,尤其是动态的表单数据字段。
要使用序列化字段的功能,只需要在模型中定义serializeField属性,定义格式如下:protected $serializeField = array(
    'info' => array('name', 'email', 'address'),
);
Info是数据表中的实际存在的字段,保存到其中的值是name、email和address三个表单字段的序列化结果。序列化字段功能可以在数据写入的时候进行自动序列化,并且在读出数据表的时候自动反序列化,这一切都无需手动进行。
下面还是是User数据表为例,假设其中并不存在name、email和address字段,但是设计了一个文本类型的info字段,那么下面的代码是可行的:$User = D("User"); // 实例化User对象
// 然后直接给数据对象赋值
$User->name = 'ThinkPHP';
$User->email = 'ThinkPHP@gmail.com';
$User->address = '上海徐汇区';
// 把数据对象添加到数据库 name email和address会自动序列化后保存到info字段
$User->add();
查询用户数据信息$User->find(8);
// 查询结果会自动把info字段的值反序列化后生成name、email和address属性
// 输出序列化字段
echo $User->name;
echo $User->email;
echo $User->address;

文本字段

ThinkPHP支持数据模型中的个别字段采用文本方式存储,这些字段就称为文本字段,通常可以用于某些Text或者Blob字段,或者是经常更新的数据表字段。
要使用文本字段非常简单,只要在模型里面定义blobFields属性就行了。例如,我们需要对Blog模型的content字段使用文本字段,那么就可以使用下面的定义:Protected  $blobFields = array('content');系统在查询和写入数据库的时候会自动检测文本字段,并且支持多个字段的定义。
需要注意的是:对于定义的文本字段并不需要数据库有对应的字段,完全是另外的。而且,暂时不支持对文本字段的搜索功能。

只读字段

只读字段用来保护某些特殊的字段值不被更改,这个字段的值一旦写入,就无法更改。
要使用只读字段的功能,我们只需要在模型中定义readonlyField属性protected $readonlyField = array('name', 'email');例如,上面定义了当前模型的name和email字段为只读字段,不允许被更改。也就是说当执行save方法之前会自动过滤到只读字段的值,避免更新到数据库。
下面举个例子说明下:$User = D("User"); // 实例化User对象
$User->find(8);
// 更改某些字段的值
$User->name = 'TOPThink';
$User->email = 'Topthink@gmail.com';
$User->address = '上海静安区';
// 保存更改后的用户数据
$User->save();
事实上,由于我们对name和email字段设置了只读,因此只有address字段的值被更新了,而name和email的值仍然还是更新之前的值。

悲观锁和乐观锁

业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。 ThinkPHP支持两种锁机制:即通常所说的 “ 悲观锁( Pessimistic Locking ) ”和 “ 乐观锁( Optimistic Locking ) ” 。
悲观锁( Pessimistic Locking )
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 通常是使用for update子句来实现悲观锁机制。
ThinkPHP支持悲观锁机制,默认情况下,是关闭悲观锁功能的,要在查询和更新的时候启用悲观锁功能,可以通过使用之前提到的查询锁定方法,例如:$User->lock(true)->save($data);// 使用悲观锁功能乐观锁( Optimistic Locking )
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
ThinkPHP也可以支持乐观锁机制,要启用乐观锁,只需要继承高级模型类并定义模型的optimLock属性,并且在数据表字段里面增加相应的字段就可以自动启用乐观锁机制了。默认的optimLock属性是lock_version,也就是说如果要在User表里面启用乐观锁机制,只需要在User表里面增加lock_version字段,如果有已经存在的其它字段作为乐观锁用途,可以修改模型类的optimLock属性即可。如果存在optimLock属性对应的字段,但是需要临时关闭乐观锁机制,把optimLock属性设置为false就可以了。

延迟更新

我们经常需要给某些数据表添加一些需要经常更新的统计字段,例如用户的积分、文件的下载次数等等,而当这些数据更新的频率比较频繁的时候,数据库的压力也随之增大不少,我们可以利用高级模型的延迟更新功能缓解。
延迟更新功能是指我们可以给统计字段的更新设置一个延迟时间,在这个时间段内所有的更新会被累积缓存起来,然后定时地统一更新数据库。这比较适合某个字段经常需要递增或者递减,并且对实时性要求没有那么严格的情况。
我们先来看递增的情况,如果我们需要给会员累积积分,可以使用$User = D("User"); // 实例化User对象
$User->where('id=3')->setInc("score",10);// 用户的积分加10
$User->where('id=3')->setInc("score",30);// 用户的积分加30
上面的操作更新了两次用户积分,并且都实时保存到数据库
如果我们使用延迟更新方法,例如下面对用户的积分延迟更新60秒$User->where('id=3')->setLazyInc("score",10,60);
$User->where('id=3')->setLazyInc("score",30,60);
$User->where('id=3')->setLazyInc("score",10,60);
那么60秒内执行的所有积分更新操作都会被延迟,实际会在60秒后统一更新积分到数据库,而不是每次都更新数据库。临时积分会被累积并缓存起来,最后到了延迟更新时间,再统一更新。相当于在60秒后执行了:$User->where('id=3')->setInc("score",50);效果是等效。区别在于用户数据库中的积分不是实时的。
同样,还可以使用setLazyDec进行延迟更新操作。

数据分表

对于大数据量的应用,经常会对数据进行分表,有些情况是可以利用数据库的分区功能,但并不是所有的数据库或者版本都支持,因此我们可以利用ThinkPHP内置的数据分表功能来实现。帮助我们更方便的进行数据的分表和读取操作。
和数据库分区功能不同,内置的数据分表功能需要根据分表规则手动创建相应的数据表。
在需要分表的模型中定义partition属性即可。protected $partition = array(
 'field' => 'name',// 要分表的字段 通常数据会根据某个字段的值按照规则进行分表
 'type' => 'md5',// 分表的规则 包括id year mod md5 函数 和首字母
 'expr' => 'name',// 分表辅助表达式 可选 配合不同的分表规则
 'num' => 'name',// 分表的数目 可选 实际分表的数量
);
定义好了分表属性后,我们就可以来进行CURD操作了,唯一不同的是,获取当前的数据表不再使用getTableName方法,而是使用getPartitionTableName方法,而且必须传入当前的数据。然后根据数据分析应该实际操作哪个数据表。因此,分表的字段值必须存在于传入的数据中,否则会进行联合查询。

返回类型

系统默认的数据库查询返回的是数组,我们可以给单个数据设置返回类型,以满足特殊情况的需要,例如:$User = M("User"); // 实例化User对象
// 返回结果是一个数组数据
$data = $User->find(6);
// 返回结果是一个stdClass对象
$data = $User->returnResult($data, "object");
// 还可以返回自定义的类
$data = $User->returnResult($data, "User");
返回自定义的User类,类的架构方法的参数是传入的数据。例如:Class User {
    public function __construct($data){
    // 对$data数据进行处理 
    }
}