ThinkPHP3.2特殊模型

Published on 2016 - 11 - 28

虚拟模型

虚拟模型是指虽然是模型类,但并不会真正的操作数据库的模型。有些时候,我们建立模型类但又不需要进行数据库操作,仅仅是借助模型类来封装一些业务逻辑,那么可以借助虚拟模型来完成。虚拟模型不会自动连接数据库,因此也不会自动检测数据表和字段信息,有两种方式可以定义虚拟模型:

继承Model类

anamespace Home\Model;
Class UserModel extends \Think\Model {
     Protected $autoCheckFields = false;
}

设置autoCheckFields属性为false后,就会关闭字段信息的自动检测,因为ThinkPHP采用的是惰性数据库连接,只要你不进行数据库查询操作,是不会连接数据库的。

不继承Model类

namespace Home\Model;
Class UserModel {
}

这种方式下面自定义模型类就是一个单纯的业务逻辑类,不能再使用模型的CURD操作方法,但是可以实例化其他的模型类进行相关操作,也可以在需要的时候直接实例化Db类进行数据库操作。

模型分层

ThinkPHP支持模型的分层 ,除了Model层之外,我们可以根据项目的需要设计和创建其他的模型层。

通常情况下,不同的分层模型仍然是继承系统的\Think\Model类或其子类,所以,其基本操作和Model类的操作是一致的。

例如在Home模块的设计中需要区分数据层、逻辑层、服务层等不同的模型层,我们可以在模块目录下面创建Model、Logic和Service目录,把对用户表的所有模型操作分成三层:

  • 数据层:Home\Model\UserModel 用于定义数据相关的自动验证和自动完成和数据存取接口
  • 逻辑层:Home\Logic\UserLogic 用于定义用户相关的业务逻辑
  • 服务层:Home\Service\UserService 用于定义用户相关的服务接口等

三个模型层的定义如下:

Model类:Home\Model\UserModel.class.php

namespace Home\Model;
class UserModel extends \Think\Model{
}

实例化方法:D('User');

Logic类:Home\Logic\UserLogic.class.php

namespace Home\Logic;
class UserLogic extends \Think\Model{
}

实例化方法:D('User','Logic');

Api类:Home\Api\UserApi.class.php

namespace Home\Api;
class UserApi extends \Think\Model{
}

实例化方法:D('User','Api');

D方法默认操作的模型层由DEFAULT_M_LAYER参数配置,我们可以改变默认操作的模型层为Logic层,例如:

'DEFAULT_M_LAYER'       =>  'Logic', // 默认的模型层名称

这样,当我们调用:

$User = D('User');

的时候其实是实例化的 UserLogic类,而不是UserModel类。

关联模型

关联关系

通常我们所说的关联关系包括下面三种:

一对一关联 :ONE_TO_ONE,包括HAS_ONE 和 BELONGS_TO 
一对多关联 :ONE_TO_MANY,包括HAS_MANY 和 BELONGS_TO
多对多关联 :MANY_TO_MANY

关联关系必然有一个参照表,例如:

  • 有一个员工档案管理系统项目,这个项目要包括下面的一些数据表:基本信息表、员工档案表、部门表、项目组表、银行卡表(用来记录员工的银行卡资料)。
  • 这些数据表之间存在一定的关联关系,我们以员工基本信息表为参照来分析和其他表之间的关联:
  • 每个员工必然有对应的员工档案资料,所以属于HAS_ONE关联;
  • 每个员工必须属于某个部门,所以属于BELONGS_TO关联;
  • 每个员工可以有多个银行卡,但是每张银行卡只可能属于一个员工,因此属于HAS_MANY关联;
  • 每个员工可以同时在多个项目组,每个项目组同时有多个员工,因此属于MANY_TO_MANY关联;
  • 分析清楚数据表之前的关联关系后,我们才可以进行关联定义和关联操作。

关联定义

ThinkPHP可以很轻松的完成数据表的关联CURD操作,目前支持的关联关系包括下面四种:

HAS_ONE、BELONGS_TO、HAS_MANY和MANY_TO_MANY

一个模型根据业务模型的复杂程度可以同时定义多个关联,不受限制,所有的关联定义都统一在模型类的 $_link 成员变量里面定义,并且可以支持动态定义。要支持关联操作,模型类必须继承Think\Model\RelationModel类,关联定义的格式是:

namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
    protected $_link = array(
         '关联1'  =>  array(
             '关联属性1' => '定义',
             '关联属性N' => '定义',
         ),
         '关联2'  =>  array(
             '关联属性1' => '定义',
             '关联属性N' => '定义',
         ),
         '关联3'  =>  HAS_ONE, // 快捷定义
         ...
    );
}

下面我们首先来分析下各个关联方式的定义:

HAS_ONE

HAS_ONE关联表示当前模型拥有一个子对象,例如,每个员工都有一个人事档案。我们可以建立一个用户模型UserModel,并且添加如下关联定义:

namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
     protected $_link = array(
        'Profile'=> self::HAS_ONE,
     );
}

上面是最简单的方式,表示其遵循了系统内置的数据库规范,完整的定义方式是:

namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
    protected $_link = array(
        'Profile'=>array(
            'mapping_type'      => self::HAS_ONE,
            'class_name'        => 'Profile',
            // 定义更多的关联属性
            ……
            ),
        );
}

关联HAS_ONE支持的关联属性有:

mapping_type :关联类型

这个在HAS_ONE 关联里面必须使用HAS_ONE 常量定义。

class_name :要关联的模型类名

例如,class_name 定义为Profile的话则表示和另外的Profile模型类关联,这个Profile模型类是无需定义的,系统会自动定位到相关的数据表进行关联。

mapping_name :关联的映射名称,用于获取数据用

该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。如果mapping_name没有定义的话,会取class_name的定义作为mapping_name。如果class_name也没有定义,则以数组的索引作为mapping_name。

foreign_key : 关联的外键名称

外键的默认规则是当前数据对象名称_id,例如: UserModel对应的可能是表think_user (注意:think只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关联的时候显式定义 foreign_key 。

condition : 关联条件

关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过定义关联的condition属性。

mapping_fields : 关联要查询的字段

默认情况下,关联查询的关联数据是关联表的全部字段,如果只是需要查询个别字段,可以定义关联的mapping_fields属性。

as_fields :直接把关联的字段值映射成数据对象中的某个字段

这个特性是ONE_TO_ONE 关联特有的,可以直接把关联数据映射到数据对象中,而不是作为一个关联数据。当关联数据的字段名和当前数据对象的字段名称有冲突时,还可以使用映射定义。

BELONGS_TO

Belongs_to 关联表示当前模型从属于另外一个父对象,例如每个用户都属于一个部门。我们可以做如下关联定义。

'Dept' => self::BELONGS_TO

完整方式定义为:

'Dept' => array(
    'mapping_type'  => self::BELONGS_TO,
    'class_name'    => 'Dept',
    'foreign_key'   => 'userId',
    'mapping_name'  => 'dept',
    // 定义更多的关联属性
    ……
),

关联BELONGS_TO定义支持的关联属性有:

属性 描述
class_name 要关联的模型类名
mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。
foreign_key 关联的外键名称
mapping_fields 关联要查询的字段
condition 关联条件
parent_key 自引用关联的关联字段 默认为parent_id 自引用关联是一种比较特殊的关联,也就是关联表就是当前表。
as_fields 直接把关联的字段值映射成数据对象中的某个字段

HAS_MANY

HAS_MANY 关联表示当前模型拥有多个子对象,例如每个用户有多篇文章,我们可以这样来定义:

'Article' => self::HAS_MANY

完整定义方式为:

'Article' => array(
    'mapping_type'  => self::HAS_MANY,
    'class_name'    => 'Article',
    'foreign_key'   => 'userId',
    'mapping_name'  => 'articles',
    'mapping_order' => 'create_time desc',
    // 定义更多的关联属性
    ……
),

关联HAS_MANY定义支持的关联属性有:

属性 描述
class_name 要关联的模型类名
mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。
foreign_key 关联的外键名称
parent_key 自引用关联的关联字段 默认为parent_id
condition 关联条件 关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过定义关联的condition属性。
mapping_fields 关联要查询的字段 默认情况下,关联查询的关联数据是关联表的全部字段,如果只是需要查询个别字段,可以定义关联的mapping_fields属性。
mapping_limit 关联要返回的记录数目
mapping_order 关联查询的排序

外键的默认规则是当前数据对象名称_id,例如:UserModel对应的可能是表think_user (注意:think只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关联的时候定义 foreign_key 。

MANY_TO_MANY

MANY_TO_MANY 关联表示当前模型可以属于多个对象,而父对象则可能包含有多个子对象,通常两者之间需要一个中间表类约束和关联。例如每个用户可以属于多个组,每个组可以有多个用户:

'Group' => self::MANY_TO_MANY

完整定义方式为:

'Group' => array(
    'mapping_type'      =>  self::MANY_TO_MANY,
    'class_name'        =>  'Group',
    'mapping_name'      =>  'groups',
    'foreign_key'       =>  'userId',
    'relation_foreign_key'  =>  'groupId',
    'relation_table'    =>  'think_group_user' //此处应显式定义中间表名称,且不能使用C函数读取表前缀
    )

MANY_TO_MANY支持的关联属性定义有:

属性 描述
class_name 要关联的模型类名
mapping_name 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。
foreign_key 关联的外键名称 外键的默认规则是当前数据对象名称_id
relation_foreign_key 关联表的外键名称 默认的关联表的外键名称是表名_id
mapping_limit 关联要返回的记录数目
mapping_order 关联查询的排序
relation_table 多对多的中间关联表名称

多对多的中间表默认表规则是:数据表前缀关联操作的主表名关联表名

如果think_user 和 think_group 存在一个对应的中间表,默认的表名应该是 如果是由group来操作关联表,中间表应该是 think_group_user,如果是从user表来操作,那么应该是think_user_group,也就是说,多对多关联的设置,必须有一个Model类里面需要显式定义中间表,否则双向操作会出错。 中间表无需另外的id主键(但是这并不影响中间表的操作),通常只是由 user_id 和 group_id 构成。 默认会通过当前模型的getRelationTableName方法来自动获取,如果当前模型是User,关联模型是Group,那么关联表的名称也就是使用 user_group这样的格式,如果不是默认规则,需要指定relation_table属性。

3.2.2版本开始,relation_table定义支持简化写法,例如:

'relation_table'=>'__USER_GROUP__'

关联查询

由于性能问题,新版取消了自动关联查询机制,而统一使用relation方法进行关联操作,relation方法不但可以启用关联还可以控制局部关联操作,实现了关联操作一切尽在掌握之中。

$User = D("User");
$user = $User->relation(true)->find(1);

输出$user结果可能是类似于下面的数据:

array(
    'id'        => 1,
    'account'   => 'ThinkPHP',
    'password'  => '123456',
    'Profile'   => array(
        'email'     => 'liu21st@gmail.com',
        'nickname'  => '流年',
        ),
    )

我们可以看到,用户的关联数据已经被映射到数据对象的属性里面了。其中Profile就是关联定义的mapping_name属性。

如果我们按照下面的方式定义了as_fields属性的话,

protected $_link = array(
    'Profile'=>array(
        'mapping_type'  => self::HAS_ONE,
        'class_name'    => 'Profile',
        'foreign_key'   => 'userId',
        'as_fields' => 'email,nickname',
        ),
    );

查询的结果就变成了下面的结果

array(
    'id'        => 1,
    'account'   => 'ThinkPHP',
    'password'  => 'name',
    'email'     => 'liu21st@gmail.com',
    'nickname'  => '流年',
    )

email和nickname两个字段已经作为user数据对象的字段来显示了。

如果关联数据的字段名和当前数据对象的字段有冲突的话,怎么解决呢?

我们可以用下面的方式来变化下定义:

'as_fields' => 'email,nickname:username',

表示关联表的nickname字段映射成当前数据对象的username字段。

默认会把所有定义的关联数据都查询出来,有时候我们并不希望这样,就可以给relation方法传入参数来控制要关联查询的。

$User = D("User");
$user = $User->relation('Profile')->find(1);

关联查询一样可以支持select方法,如果要查询多个数据,并同时获取相应的关联数据,可以改成:

$User = D("User");
$list = $User->relation(true)->Select();

如果希望在完成的查询基础之上 再进行关联数据的查询,可以使用

$User = D("User");
$user = $User->find(1);
// 表示对当前查询的数据对象进行关联数据获取
$profile = $User->relationGet("Profile");

事实上,除了当前的参考模型User外,其他的关联模型是不需要创建的。

关联操作

除了关联查询外,系统也支持关联数据的自动写入、更新和删除

关联写入

$User = D("User");
$data = array();
$data["account"]    = "ThinkPHP";
$data["password"]   = "123456";
$data["Profile"]    = array(
  'email'    =>'liu21st@gmail.com',
  'nickname'    =>'流年',
);
$result = $User->relation(true)->add($data);

这样就会自动写入关联的Profile数据。

同样,可以使用参数来控制要关联写入的数据:

$result = $User->relation("Profile")->add($data);

当MANY_TO_MANY时,不建议使用关联插入。

关联更新

数据的关联更新和关联写入类似

$User = D("User");
$data["account"]    = "ThinkPHP";
$data["password"]   = "123456";
$data["Profile"]    = array(
     'email'    =>'liu21st@gmail.com',
     'nickname'    =>'流年',
);
$result = $User-> relation(true)->where(array('id'=>3))->save($data);

Relation(true)会关联保存User模型定义的所有关联数据,如果只需要关联保存部分数据,可以使用:

$result = $User->relation("Profile")->save($data);

这样就只会同时更新关联的Profile数据。

关联保存的规则:

HAS_ONE: 关联数据的更新直接赋值
HAS_MANY: 的关联数据如果传入主键的值 则表示更新 否则就表示新增
MANY_TO_MANY: 的数据更新是删除之前的数据后重新写入

关联删除

//删除用户ID为3的记录的同时删除关联数据
$result = $User->relation(true)->delete("3");
// 如果只需要关联删除部分数据,可以使用
$result = $User->relation("Profile")->delete("3");

视图模型

视图定义

视图在有些数据库下面并不被支持,但是ThinkPHP模拟实现了数据库的视图,该功能可以用于多表联合查询。非常适合解决HAS_ONE 和 BELONGS_TO类型的关联查询。

要定义视图模型,只需要继承Think\Model\ViewModel,然后设置viewFields属性即可。

例如下面的例子,我们定义了一个BlogView模型对象,其中包括了Blog模型的id、name、title和User模型的name,以及Category模型的title字段,我们通过创建BlogView模型来快速读取一个包含了User名称和类别名称的Blog记录(集)。

namespace Home\Model;
use Think\Model\ViewModel;
class BlogViewModel extends ViewModel {
   public $viewFields = array(
     'Blog'=>array('id','name','title'),
     'Category'=>array('title'=>'category_name', '_on'=>'Blog.category_id=Category.id'),
     'User'=>array('name'=>'username', '_on'=>'Blog.user_id=User.id'),
   );
 }

$viewFields 属性表示视图模型包含的字段,每个元素定义了某个数据表或者模型的字段。

例如:

'Blog'=>array('id','name','title');

表示BlogView视图模型要包含Blog模型中的id、name和title字段属性,这个其实很容易理解,就和数据库的视图要包含某个数据表的字段一样。而Blog相当于是给Blog模型对应的数据表定义了一个别名。

默认情况下会根据定义的名称自动获取表名,如果希望指定数据表,可以使用:

'_table'=>"test_user"
// 或者使用简化定义(自动获取表前缀)
// '_table'=>"__USER__"

如果希望给当前数据表定义另外的别名,可以使用

'_as'=>'myBlog'

BlogView视图模式除了包含Blog模型之外,还包含了Category和User模型,下面的定义:

'Category'=>array('title'=>'category_name');

和上面类似,表示BlogView视图模型还要包含Category模型的title字段,因为视图模型里面已经存在了一个title字段,所以我们通过

'title'=>'category_name'

把Category模型的title字段映射为category_name字段,如果有多个字段,可以使用同样的方式添加。

可以通过_on来给视图模型定义关联查询条件,例如:

'_on'=>'Blog.category_id=Category.id'

理解之后,User模型的定义方式同样也就很容易理解了。

Blog.categoryId=Category.id AND Blog.userId=User.id

最后,我们把视图模型的定义翻译成SQL语句就更加容易理解视图模型的原理了。假设我们不带任何其他条件查询全部的字段,那么查询的SQL语句就是

Select 
 Blog.id as id,
 Blog.name as name,
 Blog.title as title,
 Category.title as category_name,
 User.name as username 
 from think_blog Blog JOIN think_category Category JOIN think_user User 
 where Blog.category_id=Category.id AND Blog.user_id=User.id

视图模型的定义并不需要先单独定义其中的模型类,系统会默认按照系统的规则进行数据表的定位。如果Blog模型并没有定义,那么系统会自动根据当前模型的表前缀和后缀来自动获取对应的数据表。也就是说,如果我们并没有定义Blog模型类,那么上面的定义后,系统在进行视图模型的操作的时候会根据Blog这个名称和当前的表前缀设置(假设为Think_ )获取到对应的数据表可能是think_blog。

ThinkPHP还可以支持视图模型的JOIN类型定义,我们可以把上面的视图定义改成:

 public $viewFields = array(
    'Blog'=>array('id','name','title','_type'=>'LEFT'),
    'Category'=>array('title'=>'category_name','_on'=>'Category.id=Blog.category_id','_type'=>'RIGHT'),
    'User'=>array('name'=>'username','_on'=>'User.id=Blog.user_id'),
   );

需要注意的是,这里的_type定义对下一个表有效,因此要注意视图模型的定义顺序。Blog模型的

'_type'=>'LEFT'

针对的是下一个模型Category而言,通过上面的定义,我们在查询的时候最终生成的SQL语句就变成:

Select 
Blog.id as id,
Blog.name as name,
Blog.title as title,
Category.title as category_name,
User.name as username 
from think_blog Blog LEFT JOIN think_category Category ON Blog.category_id=Category.id RIGHT JOIN think_user User ON Blog.user_id=User.id

我们可以在试图模型里面定义特殊的字段,例如下面的例子定义了一个统计字段

'Category'=>array('title'=>'category_name','COUNT(Blog.id)'=>'count','_on'=>'Category.id=Blog.category_id'),

视图查询

接下来,我们就可以和使用普通模型一样对视图模型进行操作了 。

$Model = D("BlogView");
$Model->field('id,name,title,category_name,username')->where('id>10')->order('id desc')->select();

看起来和普通的模型操作并没有什么大的区别,可以和使用普通模型一样进行查询。如果发现查询的结果存在重复数据,还可以使用group方法来处理。

$Model->field('id,name,title,category_name,username')->order('id desc')->group('id')->select();

我们可以看到,即使不定义视图模型,其实我们也可以通过方法来操作,但是显然非常繁琐。

$Model = D("Blog");
$Model->table('think_blog Blog,think_category Category,think_user User')
 ->field('Blog.id,Blog.name,Blog.title,Category.title as category_name,User.name as username')
 ->order('Blog.id desc')
 ->where('Blog.category_id=Category.id AND Blog.user_id=User.id')
 ->select();

而定义了视图模型之后,所有的字段会进行自动处理,添加表别名和字段别名,从而简化了原来视图的复杂查询。如果不使用视图模型,也可以用连贯操作的JOIN方法实现相同的功能。

高级模型

高级模型提供了更多的查询功能和模型增强功能,利用了模型类的扩展机制实现。如果需要使用高级模型的下面这些功能,记得需要继承Think\Model\AdvModel类或者采用动态模型。

namespace Home\Model;
use Think\Model\AdvModel;
class UserModel extends AdvModel{
 }

我们下面的示例都假设UserModel类继承自Think\Model\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->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->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就可以了。

数据分表

对于大数据量的应用,经常会对数据进行分表,有些情况是可以利用数据库的分区功能,但并不是所有的数据库或者版本都支持,因此我们可以利用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数据进行处理 
    }
 } 

mongo模型

Mongo模型是专门为Mongo数据库驱动而支持的Model扩展,如果需要操作Mongo数据库的话,自定义的模型类必须继承Think\Model\MongoModel。

Mongo模型为操作Mongo数据库提供了更方便的实用功能和查询用法,包括:

  1. 对MongoId对象和非对象主键的全面支持;
  2. 保持了动态追加字段的特性;
  3. 数字自增字段的支持;
  4. 执行SQL日志的支持;
  5. 字段自动检测的支持;
  6. 查询语言的支持;
  7. MongoCode执行的支持;

主键

系统很好的支持Mongo的主键类型,Mongo默认的主键名是 _id,也可以通过设置pk属性改变主键名称(也许你需要用其他字段作为数据表的主键),例如:

namespace Home\Model;
use Think\Model\MongoModel;
Class UserModel extends MongoModel {
    Protected $pk = 'id';
}

主键支持三种类型(通过_idType属性设置),分别是:

类型 描述
self::TYPE_OBJECT或者1 (默认类型) 采用MongoId对象,写入或者查询的时候传入数字或者字符会自动转换,获取的时候会自动转换成字符串。
self::TYPE_INT或者2 整形,支持自动增长,通过设置_autoInc 属性
self::TYPE_STRING或者3 字符串hash

设置主键类型示例:

namespace Home\Model;
use Think\Model\MongoModel;
Class UserModel extends MongoModel {
     Protected $_idType = self::TYPE_INT;
     protected $_autoinc =  true;
}

字段检测

MongoModel默认关闭字段检测,是为了保持Mongo的动态追加字段的特性,如果你的应用不需要使用Mongo动态追加字段的特性,可以设置autoCheckFields为true即可开启字段检测功能,提高安全性。一旦开启字段检测功能后,系统会自动查找当前数据表的第一条记录来获取字段列表。

如果你关闭字段检测功能的话,将不能使用查询的字段排除功能。

连贯操作

MongoModel中有部分连贯操作暂时不支持,包括:group、union、join、having、lock和distinct操作。其他连贯操作都可以很好的支持,例如:

$Model = new Think\Model\MongoModel("User");
$Model->field("name,email,age")->order("status desc")->limit("10,8")->select();

查询支持

Mongo数据库的查询条件和其他数据库有所区别。

  1. 首先,支持所有的普通查询和快捷查询;
  2. 表达式查询增加了一些针对MongoDb的查询用法;
  3. 统计查询目前只能支持count操作,其他的可能要自己通过MongoCode来实现了;

MongoModel的组合查询支持

_string 采用MongoCode查询
_query 和其他数据库的请求字符串查询相同
_complex MongoDb暂不支持

MongoModel提供了MongoCode方法,可以支持MongoCode方式的查询或者操作。

表达式查询

表达式查询采用下面的方式:

$map['字段名'] = array('表达式','查询条件');

因为MongoDb的特性,MongoModel的表达式查询和其他的数据库有所区别,增加了一些新的用法。

表达式不分大小写,支持的查询表达式和Mongo原生的查询语法对照如下:

查询表达式 含义 Mongo原生查询条件
neq 或者ne 不等于 $ne
lt 小于 $lt
lte 或者elt 小于等于 $lte
gt 大于 $gt
gte 或者egt 大于等于 $gte
like 模糊查询 用MongoRegex正则模拟
mod 取模运算 $mod
in in查询 $in
nin或者not in not in查询 $nin
all 满足所有条件 $all
between 在某个的区间
not between 不在某个区间
exists 字段是否存在 $exists
size 限制属性大小 $size
type 限制字段类型 $type
regex MongoRegex正则查询 MongoRegex实现
exp 使用MongoCode查询

注意,在使用like查询表达式的时候,和mysql的方式略有区别,对应关系如下:

Mysql模糊查询 Mongo模糊查询
array('like','%thinkphp%'); array('like','thinkphp');
array('like','thinkphp%'); array('like','^thinkphp');
array('like','%thinkphp'); array('like','thinkphp$');

参考文档