ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
在ThinkPHP中基础的模型类就是`Think\Model`类,该类完成了基本的CURD、ActiveRecord模式、连贯操作和统计查询,一些高级特性被封装到另外的模型扩展中。 基础模型类的设计非常灵活,甚至可以无需进行任何模型定义,就可以进行相关数据表的ORM和CURD操作,只有在需要封装单独的业务逻辑的时候,模型类才是必须被定义的。 # 定义与实例化(虚拟模型) ## 模型定义 > 模型类并非必须定义,只有当存在独立的业务逻辑或者属性的时候才需要定义。 模型类通常需要继承系统的\Think\Model类或其子类,下面是一个Home\Model\UserModel类的定义: ~~~ namespace Home\Model; use Think\Model; class UserModel extends Model { } ~~~ 模型类的作用大多数情况是操作数据表的,如果按照系统的规范来命名模型类的话,大多数情况下是可以自动对应数据表。 模型类的命名规则是除去表前缀的数据表名称,采用驼峰法命名,并且首字母大写,然后加上模型层的名称(默认定义是Model),例如: | 模型名 | 约定对应数据表(假设数据库的前缀定义是 think_) | |-----|-----| | UserModel | think_user | | UserTypeModel | think_user_type | 如果你的规则和上面的系统约定不符合,那么需要设置Model类的数据表名称属性,以确保能够找到对应的数据表。 ### 数据表定义 在ThinkPHP的模型里面,有几个关于数据表名称的属性定义: | 属性 | 说明 | |-----|-----| | tablePrefix | 定义模型对应数据表的前缀,如果未定义则获取配置文件中的DB_PREFIX参数 | | tableName | 不包含表前缀的数据表名称,一般情况下默认和模型名称相同,只有当你的表名和当前的模型类的名称不同的时候才需要定义。 | | trueTableName | 包含前缀的数据表名称,也就是数据库中的实际表名,该名称无需设置,只有当上面的规则都不适用的情况或者特殊情况下才需要设置。 | | dbName | 定义模型当前对应的数据库名称,只有当你当前的模型类对应的数据库名称和配置文件不同的时候才需要定义。 | 举个例子来加深理解,例如,在数据库里面有一个`think_categories`表,而我们定义的模型类名称是`CategoryModel`,按照系统的约定,这个模型的名称是Category,对应的数据表名称应该是`think_category`(全部小写),但是现在的数据表名称是`think_categories`,因此我们就需要设置`tableName`属性来改变默认的规则(假设我们已经在配置文件里面定义了`DB_PREFIX` 为 think_)。 ~~~ namespace Home\Model; use Think\Model; class CategoryModel extends Model { protected $tableName = 'categories'; } ~~~ 注意这个属性的定义不需要加表的前缀`think_` 如果我们需要CategoryModel模型对应操作的数据表是 `top_category`,那么我们只需要设置数据表前缀即可: ~~~ namespace Home\Model; use Think\Model; class CategoryModel extends Model { protected $tablePrefix = 'top_'; } ~~~ 如果你的数据表直接就是`category`,而没有前缀,则可以设置`tablePrefix`为空字符串。 ~~~ namespace Home\Model; use Think\Model; class CategoryModel extends Model { protected $tablePrefix = ''; } ~~~ > 没有表前缀的情况必须设置,否则会获取当前配置文件中的 `DB_PREFIX`。 而对于另外一种特殊情况,我们需要操作的数据表是`top_categories`,这个时候我们就需要定义 `trueTableName` 属性 ~~~ namespace Home\Model; use Think\Model; class CategoryModel extends Model { protected $trueTableName = 'top_categories'; } ~~~ > 注意`trueTableName`需要完整的表名定义。 除了数据表的定义外,还可以对数据库进行定义(用于操作当前数据库以外的数据表),例如 `top.top_categories`: ~~~ namespace Home\Model; use Think\Model; class CategoryModel extends Model { protected $trueTableName = 'top_categories'; protected $dbName = 'top'; } ~~~ > 系统的规则下,tableName会转换为小写定义,但是trueTableName定义的数据表名称是保持原样。因此,如果你的数据表名称需要区分大小写的情况,那么可以通过设置trueTableName定义来解决。 ## 实例化 在ThinkPHP中,可以无需进行任何模型定义。只有在需要封装单独的业务逻辑的时候,模型类才是必须被定义的,因此ThinkPHP在模型上有很多的灵活和方便性,让你无需因为表太多而烦恼。 根据不同的模型定义,我们有几种实例化模型的方法,根据需要采用不同的方式: ### 直接实例化 可以和实例化其他类库一样实例化模型类,例如: ~~~ $User = new \Home\Model\UserModel(); $Info = new \Admin\Model\InfoModel(); // 带参数实例化 $New = new \Home\Model\NewModel('blog','think_',$connection); ~~~ 模型类通常都是继承系统的\Think\Model类,该类的架构方法有三个参数,分别是: `Model(['模型名'],['数据表前缀'],['数据库连接信息']);` 三个参数都是可选的,大多数情况下,我们根本无需传入任何参数即可实例化。 | 参数 | 描述 | |-----|-----| | 模型名 | 模型的名称 和数据表前缀一起配合用于自动识别数据表名称 | | 数据表前缀 | 当前数据表前缀 和模型名一起配合用于自动识别数据表名称 | | 数据库连接信息 | 当前数据表的数据库连接信息 如果没有则获取配置文件中的 | > 数据表前缀传入空字符串表示取当前配置的表前缀,如果当前数据表没有前缀,则传入null即可。 数据库连接信息参数支持三种格式: ##### 1、字符串定义 字符串定义采用DSN格式定义,格式定义规范为: `数据库类型://用户名:密码@数据库主机名或者IP:数据库端口/数据库名#字符集` 例如: ~~~ new \Home\Model\NewModel('blog','think_','mysql://root:1234@localhost/demo'); ~~~ ##### 2、数组定义 可以传入数组格式的数据库连接信息,例如: ~~~ $connection = array( 'db_type' => 'mysql', 'db_host' => '127.0.0.1', 'db_user' => 'root', 'db_pwd' => '12345', 'db_port' => 3306, 'db_name' => 'demo', 'db_charset' => 'utf8', ); new \Home\Model\NewModel('new','think_',$connection); ~~~ 如果需要的话,还可以传入更多的连接参数,包括数据的部署模式和调试模式设定,例如: ~~~ $connection = array( 'db_type' => 'mysql', 'db_host' => '192.168.1.2,192.168.1.3', 'db_user' => 'root', 'db_pwd' => '12345', 'db_port' => 3306, 'db_name' => 'demo', 'db_charset' => 'utf8', 'db_deploy_type'=> 1, 'db_rw_separate'=> true, 'db_debug' => true, ); // 分布式数据库部署 并且采用读写分离 开启数据库调试模式 new \Home\Model\NewModel('new','think_',$connection); ~~~ > 注意,如果设置了db\_debug参数,那么数据库调试模式就不再受APP\_DEBUG常量影响。 ##### 3、配置定义 我们可以事先在配置文件中定义好数据库连接信息,然后在实例化的时候直接传入配置的名称即可,例如: ~~~ //数据库配置1 'DB_CONFIG1' => array( 'db_type' => 'mysql', 'db_user' => 'root', 'db_pwd' => '1234', 'db_host' => 'localhost', 'db_port' => '3306', 'db_name' => 'thinkphp' ), //数据库配置2 'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp', ~~~ 在配置文件中定义数据库连接信息的时候也支持字符串和数组格式,格式和上面实例化传入的参数一样。 然后,我们就可以这样实例化模型类传入连接信息: ~~~ new \Home\Model\NewModel('new','think_','DB_CONFIG1'); new \Home\Model\BlogModel('blog','think_','DB_CONFIG2'); ~~~ 事实上,当我们实例化的时候没有传入任何的数据库连接信息的时候,系统其实默认会获取配置文件中的相关配置参数,包括: ~~~ 'DB_TYPE' => '', // 数据库类型 'DB_HOST' => '', // 服务器地址 'DB_NAME' => '', // 数据库名 'DB_USER' => '', // 用户名 'DB_PWD' => '', // 密码 'DB_PORT' => '', // 端口 'DB_PREFIX' => '', // 数据库表前缀 'DB_DSN' => '', // 数据库连接DSN 用于PDO方式 'DB_CHARSET' => 'utf8', // 数据库的编码 默认为utf8 ~~~ 如果应用配置文件中有配置上述数据库连接信息的话,实例化模型将会变得非常简单。 ## D方法实例化 上面实例化的时候我们需要传入完整的类名,系统提供了一个快捷方法D用于数据模型的实例化操作。 要实例化自定义模型类,可以使用下面的方式: ~~~ <?php //实例化模型 $User = D('User'); // 相当于 $User = new \Home\Model\UserModel(); // 执行具体的数据操作 $User->select(); ~~~ > 当 `\Home\Model\UserModel` 类不存在的时候,D函数会尝试实例化公共模块下面的 `\Common\Model\UserModel` 类。 D方法的参数就是模型的名称,并且和模型类的大小写定义是一致的,例如: | 参数 | 实例化的模型文件(假设当前模块为Home) | |-----|-----| | User | 对应的模型类文件的 \Home\Model\UserModel.class.php | | UserType | 对应的模型类文件的 \Home\Model\UserTypeModel.class.php | > 如果在Linux环境下面,一定要注意D方法实例化的时候的模型名称的大小写。 D方法可以自动检测模型类,如果存在自定义的模型类,则实例化自定义模型类,如果不存在,则会实例化系统的\Think\Model基类,同时对于已实例化过的模型,不会重复实例化。 ~~~ D方法还可以支持跨模块调用,需要使用: //实例化Admin模块的User模型 D('Admin/User'); //实例化Extend扩展命名空间下的Info模型 D('Extend://Editor/Info'); ~~~ > 注意:跨模块实例化模型类的时候 不支持自动加载公共模块的模型类。 ### M方法实例化模型 D方法实例化模型类的时候通常是实例化某个具体的模型类,如果你仅仅是对数据表进行基本的CURD操作的话,使用M方法实例化的话,由于不需要加载具体的模型类,所以性能会更高。 例如: ~~~ // 使用M方法实例化 $User = M('User'); // 和用法 $User = new \Think\Model('User'); 等效 // 执行其他的数据操作 $User->select(); ~~~ M方法也可以支持跨库操作,例如: ~~~ // 使用M方法实例化 操作db_name数据库的ot_user表 $User = M('db_name.User','ot_'); // 执行其他的数据操作 $User->select(); ~~~ M方法的参数和\Think\Model类的参数是一样的,也就是说,我们也可以这样实例化: ~~~ $New = M('new','think_',$connection); // 等效于 $New = new \Think\Model('new','think_',$connection); ~~~ 具体的参数含义可以参考前面的介绍。 M方法实例化的时候,默认情况下是直接实例化系统的\Think\Model类,如果我们希望实例化其他的公共模型类的话,可以使用如下方法: ~~~ $User = M('\Home\Model\CommonModel:User','think_','db_config'); // 相当于 $User = new \Home\Model\CommonModel('User','think_','db_config'); ~~~ > 如果你的模型类有自己的业务逻辑,M方法是无法支持的,就算是你已经定义了具体的模型类,M方法实例化的时候是会直接忽略。 ### 实例化空模型类 如果你仅仅是使用原生SQL查询的话,不需要使用额外的模型类,实例化一个空模型类即可进行操作了,例如: ~~~ //实例化空模型 $Model = new Model(); //或者使用M快捷方法是等效的 $Model = M(); //进行原生的SQL查询 $Model->query('SELECT * FROM think_user WHERE status = 1'); ~~~ > 实例化空模型类后还可以用table方法切换到具体的数据表进行操作 我们在实例化的过程中,经常使用D方法和M方法,这两个方法的区别在于M方法实例化模型无需用户为每个数据表定义模型类,如果D方法没有找到定义的模型类,则会自动调用M方法。 # 连接数据库 ThinkPHP内置了抽象数据库访问层,把不同的数据库操作封装起来,我们只需要使用公共的Db类进行操作,而无需针对不同的数据库写不同的代码和底层实现,Db类会自动调用相应的数据库驱动来处理。目前包含了Mysql、SqlServer、PgSQL、Sqlite、Oracle、Ibase、Mongo等数据库的支持,并且采用PDO方式。 如果应用需要使用数据库,必须配置数据库连接信息,数据库的配置文件有多种定义方式。 ## 一、全局配置定义 常用的配置方式是在应用配置文件或者模块配置文件中添加下面的配置参数: ~~~ //数据库配置信息 'DB_TYPE' => 'mysql', // 数据库类型 'DB_HOST' => '127.0.0.1', // 服务器地址 'DB_NAME' => 'thinkphp', // 数据库名 'DB_USER' => 'root', // 用户名 'DB_PWD' => '123456', // 密码 'DB_PORT' => 3306, // 端口 'DB_PREFIX' => 'think_', // 数据库表前缀 'DB_CHARSET'=> 'utf8', // 字符集 'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志 ~~~ 数据库的类型由**DB_TYPE**参数设置。 配置文件定义的数据库连接信息一般是系统默认采用的,因为一般一个应用的数据库访问配置是相同的。该方法系统在连接数据库的时候会自动获取,无需手动连接。 可以对每个模块定义不同的数据库连接信息,如果开启了调试模式的话,还可以在不同的应用状态的配置文件里面定义独立的数据库配置信息。 ## 二、模型类定义 如果在某个模型类里面定义了`connection`属性的话,则实例化该自定义模型的时候会采用定义的数据库连接信息,而不是配置文件中设置的默认连接信息,通常用于某些数据表位于当前数据库连接之外的其它数据库,例如: ~~~ //在模型里单独设置数据库连接信息 namespace Home\Model; use Think\Model; class UserModel extends Model{ protected $connection = array( 'db_type' => 'mysql', 'db_user' => 'root', 'db_pwd' => '1234', 'db_host' => 'localhost', 'db_port' => '3306', 'db_name' => 'thinkphp', 'db_charset' => 'utf8', ); } ~~~ 也可以采用字符串方式定义,定义格式为: ##### 数据库类型://用户名:密码@数据库地址:数据库端口/数据库名#字符集 例如: ~~~ //在模型里单独设置数据库连接信息 namespace Home\Model; use Think\Model; class UserModel extends Model{ //或者使用字符串定义 protected $connection = 'mysql://root:1234@localhost:3306/thinkphp#utf8'; } ~~~ 如果我们已经在配置文件中配置了额外的数据库连接信息,例如: ~~~ //数据库配置1 'DB_CONFIG1' => array( 'db_type' => 'mysql', 'db_user' => 'root', 'db_pwd' => '1234', 'db_host' => 'localhost', 'db_port' => '3306', 'db_name' => 'thinkphp', 'db_charset'=> 'utf8', ), //数据库配置2 'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp#utf8'; ~~~ 那么,我们可以把模型类的属性定义改为: ~~~ //在模型里单独设置数据库连接信息 namespace Home\Model; use Think\Model; class UserModel extends Model{ //调用配置文件中的数据库配置1 protected $connection = 'DB_CONFIG1'; } ~~~ ~~~ //在模型里单独设置数据库连接信息 namespace Home\Model; use Think\Model; class InfoModel extends Model{ //调用配置文件中的数据库配置1 protected $connection = 'DB_CONFIG2'; } ~~~ ## 三、实例化定义 除了在模型定义的时候指定数据库连接信息外,我们还可以在实例化的时候指定数据库连接信息,例如: 如果采用的是M方法实例化模型的话,也可以支持传入不同的数据库连接信息,例如: ~~~ $User = M('User','other_','mysql://root:1234@localhost/demo#utf8'); ~~~ 表示实例化User模型,连接的是demo数据库的other_user表,采用的连接信息是第三个参数配置的。如果我们在项目配置文件中已经配置了`DB_CONFIG2`的话,也可以采用: ~~~ $User = M('User','other_','DB_CONFIG2'); ~~~ > 需要注意的是,ThinkPHP的数据库连接是惰性的,所以并不是在实例化的时候就连接数据库,而是在有实际的数据操作的时候才会去连接数据库(额外的情况是,在系统第一次实例化模型的时候,会自动连接数据库获取相关模型类对应的数据表的字段信息)。 # 连贯操作 ThinkPHP模型基础类提供的连贯操作方法(也有些框架称之为链式操作),可以有效的提高数据存取的代码清晰度和开发效率,并且支持所有的CURD操作。 使用也比较简单, 假如我们现在要查询一个User表的满足状态为1的前10条记录,并希望按照用户的创建时间排序 ,代码如下: ~~~ $User->where('status=1')->order('create_time')->limit(10)->select(); ~~~ 这里的`where`、`order`和`limit`方法就被称之为连贯操作方法,除了select方法必须放到最后一个外(因为select方法并不是连贯操作方法),连贯操作的方法调用顺序没有先后,例如,下面的代码和上面的等效: ~~~ $User->order('create_time')->limit(10)->where('status=1')->select(); ~~~ 如果不习惯使用连贯操作的话,还支持直接使用参数进行查询的方式。例如上面的代码可以改写为: ~~~ $User->select(array('order'=>'create_time','where'=>'status=1','limit'=>'10')); ~~~ 使用数组参数方式的话,索引的名称就是连贯操作的方法名称。其实不仅仅是查询方法可以使用连贯操作,包括所有的CURD方法都可以使用,例如: ~~~ $User->where('id=1')->field('id,name,email')->find(); $User->where('status=1 and id=1')->delete(); ~~~ 连贯操作通常只有一个参数,并且仅在当此查询或者操作有效,完成后会自动清空连贯操作的所有传值(有个别特殊的连贯操作有多个参数,并且会记录当前的传值)。简而言之,连贯操作的结果不会带入以后的查询。 系统支持的连贯操作方法有: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | where* | 用于查询或者更新条件的定义 | 字符串、数组和对象 | | table | 用于定义要操作的数据表名称 | 字符串和数组 | | alias | 用于给当前数据表定义别名 | 字符串 | | data | 用于新增或者更新数据之前的数据对象赋值 | 数组和对象 | | field | 用于定义要查询的字段(支持字段排除) | 字符串和数组 | | order | 用于对结果排序 | 字符串和数组 | | limit | 用于限制查询结果数量 | 字符串和数字 | | page | 用于查询分页(内部会转换成limit) | 字符串和数字 | | group | 用于对查询的group支持 | 字符串 | | having | 用于对查询的having支持 | 字符串 | | join* | 用于对查询的join支持 | 字符串和数组 | | union* | 用于对查询的union支持 | 字符串、数组和对象 | | distinct | 用于查询的distinct支持 | 布尔值 | | lock | 用于数据库的锁机制 | 布尔值 | | cache | 用于查询缓存 | 支持多个参数 | | relation | 用于关联查询(需要关联模型支持) | 字符串 | | result | 用于返回数据转换 | 字符串 | | validate | 用于数据自动验证 | 数组 | | auto | 用于数据自动完成 | 数组 | | filter | 用于数据过滤 | 字符串 | | scope* | 用于命名范围 | 字符串、数组 | | bind* | 用于数据绑定操作 | 数组或多个参数 | | token | 用于令牌验证 | 布尔值 | | comment | 用于SQL注释 | 字符串 | | index | 用于数据集的强制索引(3.2.3新增) | 字符串 | | strict | 用于数据入库的严格检测(3.2.3新增) | 布尔值 | > 所有的连贯操作都返回当前的模型实例对象(this),其中带*标识的表示支持多次调用。 具体的连贯操作方法是什么用途和可用参数,参见TP手册里[连贯操作]子章节(http://document.thinkphp.cn/manual_3_2.html#continuous_operation),我就不赘述了。 # CURD 与自动验证和自动完成 ThinkPHP提供了灵活和方便的数据操作方法,对数据库操作的四个基本操作(CURD):创建、更新、读取和删除的实现是最基本的,也是必须掌握的,在这基础之上才能熟悉更多实用的数据操作方法。 CURD操作通常是可以和连贯操作配合完成的。 ## 数据创建 在进行数据操作之前,我们往往需要手动创建需要的数据,例如对于提交的表单数据: ~~~ // 获取表单的POST数据 $data['name'] = $_POST['name']; $data['email'] = $_POST['email']; // 更多的表单数据值获取 //…… ~~~ ## 创建数据对象 ThinkPHP可以帮助你快速地创建数据对象,最典型的应用就是自动根据表单数据创建数据对象,这个优势在一个数据表的字段非常之多的情况下尤其明显。 很简单的例子: ~~~ // 实例化User模型 $User = M('User'); // 根据表单提交的POST数据创建数据对象 $User->create(); ~~~ Create方法支持从其它方式创建数据对象,例如,从其它的数据对象,或者数组等 ~~~ $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->create($data); ~~~ 甚至还可以支持从对象创建新的数据对象 ~~~ // 从User数据对象创建新的Member数据对象 $User = stdClass(); $User->name = 'ThinkPHP'; $User->email = 'ThinkPHP@gmail.com'; $Member = M("Member"); $Member->create($User); ~~~ 创建完成的数据可以直接读取和修改,例如: ~~~ $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->create($data); // 创建完成数据对象后可以直接读取数据 echo $User->name; echo $User->email; // 也可以直接修改创建完成的数据 $User->name = 'onethink'; // 修改name字段数据 $User->status = 1; // 增加新的字段数据 ~~~ ## 数据操作状态 create方法的第二个参数可以指定创建数据的操作状态,默认情况下是自动判断是写入还是更新操作。 也可以显式指定操作状态,例如: ~~~ $Member = M("User"); // 指定更新数据操作状态 $Member->create($_POST,Model::MODEL_UPDATE); ~~~ 系统内置的数据操作包括`Model::MODEL_INSERT`(或者1)和`Model::MODEL_UPDATE`(或者2),当没有指定的时候,系统根据数据源是否包含主键数据来自动判断,如果存在主键数据,就当成`Model::MODEL_UPDATE`操作。 不同的数据操作状态可以定义不同的数据验证和自动完成机制,所以,你可以自定义自己需要的数据操作状态,例如,可以设置登录操作的数据状态(假设为3): ~~~ $Member = M("User"); // 指定更新数据操作状态 $Member->create($_POST,3); ~~~ 事实上,create方法所做的工作远非这么简单,在创建数据对象的同时,完成了一系列的工作,我们来看下create方法的工作流程就能明白: | 步骤 | 说明 | 返回 | |-----|-----|-----| | 1 | 获取数据源(默认是POST数组) | | | 2 | 验证数据源合法性(非数组或者对象会过滤) | 失败则返回false | | 3 | 检查字段映射 | | | 4 | 判断数据状态(新增或者编辑,指定或者自动判断) | | | 5 | 数据自动验证 | 失败则返回false | | 6 | 表单令牌验证 | 失败则返回false | | 7 | 表单数据赋值(过滤非法字段和字符串处理) | | | 8 | 数据自动完成 | | | 9 | 生成数据对象(保存在内存) | | 因此,我们熟悉的令牌验证、[自动验证](/thinkphp/thinkphp/1776)和[自动完成](/thinkphp/thinkphp/1777)功能,其实都必须通过create方法才能生效。 如果没有定义自动验证的话,create方法的返回值是创建完成的数据对象数组,例如: ~~~ $data['name'] = 'thinkphp'; $data['email'] = 'thinkphp@gmail.com'; $data['status'] = 1; $User = M('User'); $data = $User->create($data); dump($data); ~~~ 输出结果为: ~~~ array (size=3) 'name' => string 'thinkphp' (length=8) 'email' => string 'thinkphp@gmail.com' (length=18) 'status'=> int 1 ~~~ Create方法创建的数据对象是保存在内存中,并没有实际写入到数据库中,直到使用`add`或者`save`方法才会真正写入数据库。 因此在没有调用add或者save方法之前,我们都可以改变create方法创建的数据对象,例如: ~~~ $User = M('User'); $User->create(); //创建User数据对象 $User->status = 1; // 设置默认的用户状态 $User->create_time = time(); // 设置用户的创建时间 $User->add(); // 把用户对象写入数据库 ~~~ 如果只是想简单创建一个数据对象,并不需要完成一些额外的功能的话,可以使用data方法简单的创建数据对象。 使用如下: ~~~ // 实例化User模型 $User = M('User'); // 创建数据后写入到数据库 $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->data($data)->add(); ~~~ Data方法也支持传入数组和对象,使用data方法创建的数据对象不会进行自动验证和过滤操作,请自行处理。但在进行add或者save操作的时候,数据表中不存在的字段以及非法的数据类型(例如对象、数组等非标量数据)是会自动过滤的,不用担心非数据表字段的写入导致SQL错误的问题。 ### 支持的连贯操作 在执行create方法之前,我们可以调用相关的连贯操作方法,配合完成数据创建操作。 create方法支持的连贯操作方法包括: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | field | 用于定义合法的字段 | 字符串和数组 | | validate | 用于数据自动验证 | 数组 | | auto | 用于数据自动完成 | 数组 | | token | 用于令牌验证 | 布尔值 | 更多的用法参考后续的内容。 ### 字段合法性过滤 如果在create方法之前调用field方法,则表示只允许创建指定的字段数据,其他非法字段将会被过滤,例如: ~~~ $data['name'] = 'thinkphp'; $data['email'] = 'thinkphp@gmail.com'; $data['status'] = 1; $data['test'] = 'test'; $User = M('User'); $data = $User->field('name,email')->create($data); dump($data); ~~~ 输出结果为: ~~~ array (size=2) 'name' => string 'thinkphp' (length=8) 'email' => string 'thinkphp@gmail.com' (length=18) ~~~ 最终只有`name`和`email`字段的数据被允许写入,`status`和`test`字段直接被过滤了,哪怕status也是数据表中的合法字段。 如果我们有自定义模型类,对于数据新增和编辑操作的话,我们还可以直接在模型类里面通过设置`insertFields`和`updateFields`属性来定义允许的字段,例如: ~~~ namespace Home\Model; use Think\Model; class UserModel extends Model{ protected $insertFields = 'name,email'; // 新增数据的时候允许写入name和email字段 protected $updateFields = 'email'; // 编辑数据的时候只允许写入email字段 } ~~~ ## 数据新增 ThinkPHP的数据写入操作使用**add方法**,使用示例如下: ~~~ $User = M("User"); // 实例化User对象 $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->add($data); ~~~ 如果是Mysql数据库的话,还可以支持在数据插入时允许更新操作: ~~~ add($data='',$options=array(),$replace=false) ~~~ 其中add方法增加$replace参数(是否添加数据时允许覆盖),true表示覆盖,默认为false 或者使用data方法连贯操作 ~~~ $User = M("User"); // 实例化User对象 $User->data($data)->add(); ~~~ 如果在add之前已经创建数据对象的话(例如使用了create或者data方法),add方法就不需要再传入数据了。 使用create方法的例子: ~~~ $User = M("User"); // 实例化User对象 // 根据表单提交的POST数据创建数据对象 if($User->create()){ $result = $User->add(); // 写入数据到数据库 if($result){ // 如果主键是自动增长型 成功后返回值就是最新插入的值 $insertId = $result; } } ~~~ > create方法并不算是连贯操作,因为其返回值可能是布尔值,所以必须要进行严格判断。 ### 支持的连贯操作 在执行add方法之前,我们可以调用相关的连贯操作方法,配合完成数据写入操作。 写入操作支持的连贯操作方法包括: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | table | 用于定义要操作的数据表名称 | 字符串和数组 | | data | 用于指定要写入的数据对象 | 数组和对象 | | field | 用于定义要写入的字段 | 字符串和数组 | | relation | 用于关联查询(需要关联模型支持) | 字符串 | | validate | 用于数据自动验证 | 数组 | | auto | 用于数据自动完成 | 数组 | | filter | 用于数据过滤 | 字符串 | | scope | 用于命名范围 | 字符串、数组 | | bind | 用于数据绑定操作 | 数组 | | token | 用于令牌验证 | 布尔值 | | comment | 用于SQL注释 | 字符串 | | fetchSql | 不执行SQL而只是返回SQL | 布尔值 | 可以支持不执行SQL而只是返回SQL语句,例如: ~~~ $User = M("User"); // 实例化User对象 $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $sql = $User->fetchSql(true)->add($data); echo $sql; // 输出结果类似于 // INSERT INTO think_user (name,email) VALUES ('ThinkPHP','ThinkPHP@gmail.com') ~~~ ##### 字段过滤 如果写入了数据表中不存在的字段数据,则会被直接过滤,例如: ~~~ $data['name'] = 'thinkphp'; $data['email'] = 'thinkphp@gmail.com'; $data['test'] = 'test'; $User = M('User'); $User->data($data)->add(); ~~~ 其中test字段是不存在的,所以写入数据的时候会自动过滤掉。 > 在3.2.2版本以上,如果开启调试模式的话,则会抛出异常,提示:`非法数据对象:[test=>test]` 如果在add方法之前调用field方法,则表示只允许写入指定的字段数据,其他非法字段将会被过滤,例如: ~~~ $data['name'] = 'thinkphp'; $data['email'] = 'thinkphp@gmail.com'; $data['test'] = 'test'; $User = M('User'); $User->field('name')->data($data)->add(); ~~~ 最终只有name字段的数据被允许写入,email和test字段直接被过滤了,哪怕email也是数据表中的合法字段。 ##### 字段内容过滤 通过filter方法可以对数据的值进行过滤处理,例如: ~~~ $data['name'] = '<b>thinkphp</b>'; $data['email'] = 'thinkphp@gmail.com'; $User = M('User'); $User->data($data)->filter('strip_tags')->add(); ~~~ 写入数据库的时候会把name字段的值转化为`thinkphp`。 > filter方法的参数是一个回调类型,支持函数或者闭包定义。 ### 批量写入 在某些情况下可以支持数据的批量写入,例如: ~~~ // 批量添加数据 $dataList[] = array('name'=>'thinkphp','email'=>'thinkphp@gamil.com'); $dataList[] = array('name'=>'onethink','email'=>'onethink@gamil.com'); $User->addAll($dataList); ~~~ > **该功能需要3.2.3以上版本,3.2.3以下版本仅对mysql数据库支持** ### 数据读取 在ThinkPHP中读取数据的方式很多,通常分为读取数据、读取数据集和读取字段值。 数据查询方法支持的连贯操作方法有: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | where | 用于查询或者更新条件的定义 | 字符串、数组和对象 | | table | 用于定义要操作的数据表名称 | 字符串和数组 | | alias | 用于给当前数据表定义别名 | 字符串 | | field | 用于定义要查询的字段(支持字段排除) | 字符串和数组 | | order | 用于对结果排序 | 字符串和数组 | | group | 用于对查询的group支持 | 字符串 | | having | 用于对查询的having支持 | 字符串 | | join | 用于对查询的join支持 | 字符串和数组 | | union | 用于对查询的union支持 | 字符串、数组和对象 | | distinct | 用于查询的distinct支持 | 布尔值 | | lock | 用于数据库的锁机制 | 布尔值 | | cache | 用于查询缓存 | 支持多个参数 | | relation | 用于关联查询(需要关联模型支持) | 字符串 | | result | 用于返回数据转换 | 字符串 | | scope | 用于命名范围 | 字符串、数组 | | bind | 用于数据绑定操作 | 数组 | | comment | 用于SQL注释 | 字符串 | | fetchSql | 不执行SQL而只是返回SQL | 布尔值 | > 注意:某些情况下有些连贯操作是无效的,例如limit方法对find方法是无效的。 ### 读取数据 读取数据是指读取数据表中的一行数据(或者关联数据),主要通过`find`方法完成,例如: ~~~ $User = M("User"); // 实例化User对象 // 查找status值为1name值为think的用户数据 $data = $User->where('status=1 AND name="thinkphp"')->find(); dump($data); ~~~ find方法查询数据的时候可以配合相关的连贯操作方法,其中最关键的则是where方法,如何使用where方法我们会在[查询语言](#)章节中详细描述。 如果查询出错,find方法返回false,如果查询结果为空返回NULL,查询成功则返回一个关联数组(键值是字段名或者别名)。 如果上面的查询成功的话,会输出: ~~~ array (size=3) 'name' => string 'thinkphp' (length=8) 'email' => string 'thinkphp@gmail.com' (length=18) 'status'=> int 1 ~~~ > 即使满足条件的数据不止一个,find方法也只会返回第一条记录(可以通过order方法排序后查询)。 还可以用data方法获取查询后的数据对象(查询成功后) ~~~ $User = M("User"); // 实例化User对象 // 查找status值为1name值为think的用户数据 $User->where('status=1 AND name="thinkphp"')->find(); dump($User->data()); ~~~ ### 读取数据集 读取数据集其实就是获取数据表中的多行记录(以及关联数据),使用`select`方法,使用示例: ~~~ $User = M("User"); // 实例化User对象 // 查找status值为1的用户数据 以创建时间排序 返回10条数据 $list = $User->where('status=1')->order('create_time')->limit(10)->select(); ~~~ 如果查询出错,select的返回值是false,如果查询结果为空,则返回NULL,否则返回二维数组。 ### 读取字段值 读取字段值其实就是获取数据表中的某个列的多个或者单个数据,最常用的方法是 `getField`方法。 示例如下: ~~~ $User = M("User"); // 实例化User对象 // 获取ID为3的用户的昵称 $nickname = $User->where('id=3')->getField('nickname'); ~~~ 默认情况下,当只有一个字段的时候,返回满足条件的数据表中的该字段的第一行的值。 如果需要返回整个列的数据,可以用: ~~~ $User->getField('id',true); // 获取id数组 //返回数据格式如array(1,2,3,4,5)一维数组,其中value就是id列的每行的值 ~~~ 如果传入多个字段的话,默认返回一个关联数组: ~~~ $User = M("User"); // 实例化User对象 // 获取所有用户的ID和昵称列表 $list = $User->getField('id,nickname'); //两个字段的情况下返回的是array(`id`=>`nickname`)的关联数组,以id的值为key,nickname字段值为value ~~~ 这样返回的list是一个数组,键名是用户的id字段的值,键值是用户的昵称nickname。 如果传入多个字段的名称,例如: ~~~ $list = $User->getField('id,nickname,email'); //返回的数组格式是array(`id`=>array(`id`=>value,`nickname`=>value,`email`=>value))是一个二维数组,key还是id字段的值,但value是整行的array数组,类似于select()方法的结果遍历将id的值设为数组key ~~~ 返回的是一个二维数组,类似select方法的返回结果,区别的是这个二维数组的键名是用户的id(准确的说是getField方法的第一个字段名)。 如果我们传入一个字符串分隔符: ~~~ $list = $User->getField('id,nickname,email',':'); ~~~ 那么返回的结果就是一个数组,键名是用户id,键值是 `nickname:email`的输出字符串。 getField方法还可以支持限制数量,例如: ~~~ $this->getField('id,name',5); // 限制返回5条记录 $this->getField('id',3); // 获取id数组 限制3条记录 ~~~ 可以配合使用order方法使用。更多的查询方法可以参考[查询语言](#)章节。 ## 数据更新 ThinkPHP的数据更新操作包括更新数据和更新字段方法。 ### 更新数据 更新数据使用`save`方法,例如: ~~~ $User = M("User"); // 实例化User对象 // 要修改的数据对象属性赋值 $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->where('id=5')->save($data); // 根据条件更新记录 ~~~ 也可以改成对象方式来操作: ~~~ $User = M("User"); // 实例化User对象 // 要修改的数据对象属性赋值 $User->name = 'ThinkPHP'; $User->email = 'ThinkPHP@gmail.com'; $User->where('id=5')->save(); // 根据条件更新记录 ~~~ 数据对象赋值的方式,save方法无需传入数据,会自动识别。 > 注意:save方法的返回值是**影响的记录数**,如果返回false则表示更新出错,因此一定要用恒等来判断是否更新失败。 为了保证数据库的安全,避免出错更新整个数据表,如果没有任何更新条件,数据对象本身也不包含主键字段的话,save方法不会更新任何数据库的记录。 因此下面的代码不会更改数据库的任何记录 ~~~ $User->save($data); ~~~ 除非使用下面的方式: ~~~ $User = M("User"); // 实例化User对象 // 要修改的数据对象属性赋值 $data['id'] = 5; $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->save($data); // 根据条件保存修改的数据 ~~~ 如果id是数据表的主键的话,系统自动会把主键的值作为更新条件来更新其他字段的值。 数据更新方法支持的连贯操作方法有: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | where | 用于查询或者更新条件的定义 | 字符串、数组和对象 | | table | 用于定义要操作的数据表名称 | 字符串和数组 | | alias | 用于给当前数据表定义别名 | 字符串 | | field | 用于定义允许更新的字段 | 字符串和数组 | | order | 用于对数据排序 | 字符串和数组 | | lock | 用于数据库的锁机制 | 布尔值 | | relation | 用于关联更新(需要关联模型支持) | 字符串 | | scope | 用于命名范围 | 字符串、数组 | | bind | 用于数据绑定操作 | 数组 | | comment | 用于SQL注释 | 字符串 | | fetchSql | 不执行SQL而只是返回SQL | 布尔值 | ##### 字段和数据过滤 和add方法一样,save方法支持使用`field`方法过滤字段和`filter`方法过滤数据,例如: ~~~ $User = M("User"); // 实例化User对象 // 要修改的数据对象属性赋值 $data['name'] = 'test'; $data['email'] = '<b>test@gmail.com</b>'; $User->where('id=5')->field('email')->filter('strip_tags')->save($data); // 根据条件保存修改的数据 ~~~ 当使用field('email')的时候,只允许更新email字段的值(采用strip_tags方法过滤),name字段的值将不会被修改。 还有一种方法是通过create或者data方法创建要更新的数据对象,然后进行保存操作,这样save方法的参数可以不需要传入。 ~~~ $User = M("User"); // 实例化User对象 // 要修改的数据对象属性赋值 $data['name'] = 'ThinkPHP'; $data['email'] = 'ThinkPHP@gmail.com'; $User->where('id=5')->data($data)->save(); // 根据条件保存修改的数据 ~~~ 使用create方法的例子: ~~~ $User = M("User"); // 实例化User对象 // 根据表单提交的POST数据创建数据对象 $User->create(); $User->save(); // 根据条件保存修改的数据 ~~~ ### 更新字段 如果只是更新个别字段的值,可以使用`setField`方法。 使用示例: ~~~ $User = M("User"); // 实例化User对象 // 更改用户的name值 $User-> where('id=5')->setField('name','ThinkPHP'); ~~~ setField方法支持同时更新多个字段,只需要传入数组即可,例如: ~~~ $User = M("User"); // 实例化User对象 // 更改用户的name和email的值 $data = array('name'=>'ThinkPHP','email'=>'ThinkPHP@gmail.com'); $User-> where('id=5')->setField($data); ~~~ 而对于统计字段(通常指的是数字类型)的更新,系统还提供了`setInc`和`setDec`方法。 ~~~ $User = M("User"); // 实例化User对象 $User->where('id=5')->setInc('score',3); // 用户的积分加3 $User->where('id=5')->setInc('score'); // 用户的积分加1 $User->where('id=5')->setDec('score',5); // 用户的积分减5 $User->where('id=5')->setDec('score'); // 用户的积分减1 ~~~ 3.2.3版本开始,setInc和setDec方法支持延迟更新,用法如下: ~~~ $Article = M("Article"); // 实例化Article对象 $Article->where('id=5')->setInc('view',1); // 文章阅读数加1 $Article->where('id=5')->setInc('view',1,60); // 文章阅读数加1,并且延迟60秒更新(写入) ~~~ ## 数据删除 ThinkPHP删除数据使用delete方法,例如: ~~~ $Form = M('Form'); $Form->delete(5); ~~~ 表示删除主键为5的数据,delete方法可以删除单个数据,也可以删除多个数据,这取决于删除条件,例如: ~~~ $User = M("User"); // 实例化User对象 $User->where('id=5')->delete(); // 删除id为5的用户数据 $User->delete('1,2,5'); // 删除主键为1,2和5的用户数据 $User->where('status=0')->delete(); // 删除所有状态为0的用户数据 ~~~ delete方法的返回值是删除的记录数,如果返回值是false则表示SQL出错,返回值如果为0表示没有删除任何数据。 也可以用order和limit方法来限制要删除的个数,例如: ~~~ // 删除所有状态为0的5 个用户数据 按照创建时间排序 $User->where('status=0')->order('create_time')->limit('5')->delete(); ~~~ 为了避免错删数据,如果没有传入任何条件进行删除操作的话,不会执行删除操作,例如: ~~~ $User = M("User"); // 实例化User对象 $User->delete(); ~~~ 不会删除任何数据,如果你确实要删除所有的记录,除非使用下面的方式: ~~~ $User = M("User"); // 实例化User对象 $User->where('1')->delete(); ~~~ 数据删除方法支持的连贯操作方法有: | 连贯操作 | 作用 | 支持的参数类型 | |-----|-----|-----| | where | 用于查询或者更新条件的定义 | 字符串、数组和对象 | | table | 用于定义要操作的数据表名称 | 字符串和数组 | | alias | 用于给当前数据表定义别名 | 字符串 | | order | 用于对数据排序 | 字符串和数组 | | lock | 用于数据库的锁机制 | 布尔值 | | relation | 用于关联删除(需要关联模型支持) | 字符串 | | scope | 用于命名范围 | 字符串、数组 | | bind | 用于数据绑定操作 | 数组 | | comment | 用于SQL注释 | 字符串 | | fetchSql | 不执行SQL而只是返回SQL | 布尔值 | # 几种高级模型 ## 视图模型 ### 视图定义 视图通常是指数据库的视图,视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个或多个表,或者其它视图。分布式查询也可用于定义使用多个异类源数据的视图。如果有几台不同的服务器分别存储组织中不同地区的数据,而您需要将这些服务器上相似结构的数据组合起来,这种方式就很有用。 视图在有些数据库下面并不被支持,但是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" // 3.2.2版本以上还可以使用简化定义(自动获取表前缀) '_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方法实现相同的功能。 ## 关联模型 ### 关联关系 通常我们所说的关联关系包括下面三种: ~~~ 一对一关联 :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"); ~~~ ## 高级模型 高级模型提供了更多的查询功能和模型增强功能,利用了模型类的扩展机制实现。如果需要使用高级模型的下面这些功能,记得需要继承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对象 // 然后直接给数据对象赋值 $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就可以了。 ## 数据分表 对于大数据量的应用,经常会对数据进行分表,有些情况是可以利用数据库的分区功能,但并不是所有的数据库或者版本都支持,因此我们可以利用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数据进行处理 } } ~~~ 上面是官方的介绍,老杨被人反馈说分表手册不详细,难以实践,我就研究了下。加入了演示。 1. 分表的表名规则 `XXX_table_序号` 如 'adv_article_1'。 2. 分表的表结构必须一样,有软件的建好第一个表后,复制表给个新表名就行了 3. 写一个继承高级模型的模型类,关闭字段缓存。 4. 模型类里提供一个方法进行表名切换。 5. 进行分表查询和插入。 首先看一下效果: ![document/2015-09-22/56010e1dc8263](http://box.kancloud.cn/document_2015-09-22_56010e1dc8263.png)。 然后看下我的数据库: ![document/2015-09-22/56010e48e8fc7](http://box.kancloud.cn/document_2015-09-22_56010e48e8fc7.png) ![document/2015-09-22/56010e575221a](http://box.kancloud.cn/document_2015-09-22_56010e575221a.png) ![document/2015-09-22/56010e62b2fdf](http://box.kancloud.cn/document_2015-09-22_56010e62b2fdf.png) 成功的将第3条数据插入了`adv_article_2`里。 那么我是如何做的呢? 先建2张表: ~~~ DROP TABLE IF EXISTS `adv_article_1`; CREATE TABLE `adv_article_1` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `title` varchar(255) NOT NULL COMMENT '标题', `content` varchar(1000) DEFAULT NULL COMMENT '内容', `time` int(11) unsigned DEFAULT '0' COMMENT '添加时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for `adv_article_2` -- ---------------------------- DROP TABLE IF EXISTS `adv_article_2`; CREATE TABLE `adv_article_2` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `title` varchar(255) NOT NULL COMMENT '标题', `content` varchar(1000) DEFAULT NULL COMMENT '内容', `time` int(11) unsigned DEFAULT '0' COMMENT '添加时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ~~~ 然后是模型 AdvArticleModel: ~~~ <?php namespace Home\Model; use Think\Model\AdvModel; class AdvArticleModel extends AdvModel{ protected $tableName = 'article'; Protected $autoCheckFields = false; protected $partition = array( 'field' => 'id',// 要分表的字段 通常数据会根据某个字段的值按照规则进行分表 'type' => 'id',// 分表的规则 包括id year mod md5 函数 和首字母 'expr' => '3',// 分表辅助表达式 可选 配合不同的分表规则 'num' => '2',// 分表的数目 可选 实际分表的数量 ); //获取多表表名 public function init($data = array()){ $data = empty($data) ? $_POST : $data; $table = $this->getPartitionTableName($data); if(!empty($data)){ dump("实际的表名:{$table}"); } return $this->table($table); } //清空多表 public function emptyTable(){ $tableName = array(); for($i=0;$i<$this->partition['num'];$i++){ $tableName = $this->getTableName().'_'.($i+1); $this->execute("TRUNCATE TABLE `{$tableName}`"); } } } ~~~ `protected $tableName = 'article';` 指明了高级模型的表名。 `Protected $autoCheckFields = false;` 关闭了字段缓存。 ~~~ //获取多表表名 public function init($data = array()){ $data = empty($data) ? $_POST : $data; $table = $this->getPartitionTableName($data); if(!empty($data)){ dump("实际的表名:{$table}"); } return $this->table($table); } ~~~ 获取多表表名的方法,用于计算真实的分表表名,如果没有数据进来,就是union的联合表。如这样的:`( SELECT * FROM adv_article_1 UNION SELECT * FROM adv_article_2) AS AdvArticle'`。确定了分表名会是这样的`adv_article_2`。 接下来时清空多表的方法。为了演示方便,我设置了2个分表,然后id大于2的话进入下一个表。所以示列如果不清除数据,刷新后在有数据的情况下会进入表3。而数据库里没有这个表就会报错。 ~~~ //清空多表 public function emptyTable(){ $tableName = array(); for($i=0;$i<$this->partition['num'];$i++){ $tableName = $this->getTableName().'_'.($i+1); $this->execute("TRUNCATE TABLE `{$tableName}`"); } } ~~~ 然后看我们的高级模型控制器: ~~~ <?php namespace Home\Controller; use Think\Controller; class AdvmodelController extends Controller { public function index(){ //分表演示 C('DB_PREFIX', 'adv_'); $article = D('AdvArticle'); $article->emptyTable();//清空多表 for ($i=0; $i < 3; $i++) { //获取最新的id供分表识别表名所用 $id = $article->init()->max('id'); $id = intval($id); $targetId = $id+1; $this->show('准备插入的数据:'); $data = array('title'=>'标题'.$key, 'content'=>'内容'.$key, 'time'=>time(), 'id'=>$targetId); dump($data); $advModel = $article->init($data); $result = $advModel->add($data); if($result){ $this->show('插入数据后的id为:'.$result); }else{ $this->show('插入id为'.$targetId.'的数据失败,失败原因:'.$advModel->getError()); } $this->show('<br><br>'); } $allData = $article->init()->select(); var_dump($allData); var_dump($allData, 'object'); } } ~~~ ~~~ $article = D('AdvArticle'); $article->emptyTable();//清空多表 ~~~ 实列化高级模型。 ~~~ for ($i=0; $i < 3; $i++) { //获取最新的id供分表识别表名所用 $id = $article->init()->max('id'); $id = intval($id); $targetId = $id+1; $this->show('准备插入的数据:'); $data = array('title'=>'标题'.$key, 'content'=>'内容'.$key, 'time'=>time(), 'id'=>$targetId); dump($data); $advModel = $article->init($data); $result = $advModel->add($data); if($result){ $this->show('插入数据后的id为:'.$result); }else{ $this->show('插入id为'.$targetId.'的数据失败,失败原因:'.$advModel->getError()); } $this->show('<br><br>'); } ~~~ 伪造数据,并添加数据。注意不要以为id是主键自增,就不传入init里去获取分表名。一定要传你设置的$partition 分表属性的field里的值在数组里。 ~~~ protected $partition = array( 'field' => 'id',// 要分表的字段 通常数据会根据某个字段的值按照规则进行分表 'type' => 'id',// 分表的规则 包括id year mod md5 函数 和首字母 'expr' => '3',// 分表辅助表达式 可选 配合不同的分表规则 'num' => '2',// 分表的数目 可选 实际分表的数量 ); ~~~ 主要这个里面 其他的键如field、type、expr大家可能不懂。 num就是你有几个分表。 其实看下源码就能明白了。 ~~~ /** * 得到分表的的数据表名 * @access public * @param array $data 操作的数据 * @return string */ public function getPartitionTableName($data=array()) { // 对数据表进行分区 if(isset($data[$this->partition['field']])) { $field = $data[$this->partition['field']]; switch($this->partition['type']) { case 'id': // 按照id范围分表 $step = $this->partition['expr']; $seq = floor($field / $step)+1; break; case 'year': // 按照年份分表 if(!is_numeric($field)) { $field = strtotime($field); } $seq = date('Y',$field)-$this->partition['expr']+1; break; case 'mod': // 按照id的模数分表 $seq = ($field % $this->partition['num'])+1; break; case 'md5': // 按照md5的序列分表 $seq = (ord(substr(md5($field),0,1)) % $this->partition['num'])+1; break; default : if(function_exists($this->partition['type'])) { // 支持指定函数哈希 $fun = $this->partition['type']; $seq = (ord(substr($fun($field),0,1)) % $this->partition['num'])+1; }else{ // 按照字段的首字母的值分表 $seq = (ord($field{0}) % $this->partition['num'])+1; } } return $this->getTableName().'_'.$seq; }else{ // 当设置的分表字段不在查询条件或者数据中 // 进行联合查询,必须设定 partition['num'] $tableName = array(); for($i=0;$i<$this->partition['num'];$i++) $tableName[] = 'SELECT * FROM '.$this->getTableName().'_'.($i+1); $tableName = '( '.implode(" UNION ",$tableName).') AS '.$this->name; return $tableName; } } ~~~ 我就分别解释下`switch($this->partition['type'])`中的type: - id 就是拿id除每个表总数量的商取不超过的整数+1。比方3个记录,每个表最多2条那么就按照 1 2,3 存2个表里,所以id 得程序算出来。 - year 就是根据分表字段来算一个四位数的年份,然后expr 是多少年为间隔存一个表。所以该字段要么是时间格式的,要么是时间戳。 - mod 取模,这个简单,就是数学上的取模,`1234` 取模后对应为`1010`,那么 1 2 3 4 字段对应的表就为+1 后的 2 1 2 1 分表里。 - md5 就是将字段的值md5变32位字符后取首字母在拿首字母对应的 ASCII 码值 去模分表个数。其实就是高级取模。 - default,就是type不是上面的形式时,就当自定义函数处理,如果函数存在,拿函数处理的返回值高级取模,不存在直接拿字段高级取模处理分表名。 如果获取表名没传data或者data里不存在field键时。就union 返回一个别名 查出所有分表的数据。 至于分表处理的`returnResult`,无非就支持了对象返回和以一个类的构造方法去返回数据处理,相当于模型的后置查询操作。 ~~~ /** * 返回数据 * @access public * @param array $data 数据 * @param string $type 返回类型 默认为数组 * @return mixed */ public function returnResult($data,$type='') { if('' === $type) $type = $this->returnType; switch($type) { case 'array' : return $data; case 'object': return (object)$data; default:// 允许用户自定义返回类型 if(class_exists($type)) return new $type($data); else E(L('_CLASS_NOT_EXIST_').':'.$type); } } ~~~ 源码也就这点,没啥好讲的。其实分表的关键就是建对表,社对属性。定好分表规则,然后查出对应表。进行分表处理后插入和读取。其他和基本模型没区别。注意关闭字段缓存,不然光实例化模型就会报表名不存在了。