在数据交互层中,模型作为业务数据与数据库表之间的映射桥梁,封装了数据的处理逻辑,使开发者能够脱离复杂的 SQL 语句,以面向对象的方式操作数据。模型的合理定义与字段设置是实现这一目标的基础,而基于模型的增删改查则是业务开发中最常使用的核心能力。所以本篇作为模型系列文章的第一篇,学习核心内容将集中在模型的创建与定义规则、模型字段的设置,以及如何通过模型完成基础的新增、查询、更新和删除操作上。本篇文章将记录 ThinkPHP 模型的基础定义与常用操作的学习过程。
一、定义
模型是 ThinkORM 的一个重要组成,Db 和模型的存在只是 ThinkORM 架构设计中的职责和定位不同,Db 负责的只是数据(表)访问,模型负责的是业务数据和业务逻辑,实现了数据对象化访问的封装。
相对于 Db 类来说模型的优势主要在于:
- 支持 ActiveRecord 实现
- 灵活的事件机制
- 数据自动处理能力
- 简单直观的数据关联操作
- 封装业务逻辑
1、模型定义
定义一个模型类很简单,只需要继承 think\Model 就可以了。例如下面是一个 User 模型:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
}
在 app 目录下创建 model 目录,将 User 模型放到该目录下。
模型会自动对应数据表,模型类的命名规则是除去表前缀的数据表名称,采用驼峰法命名,并且首字母大写。例如:
|----------|------------------------------|
| 模型名 | 对应数据表(假设数据库的前缀定义是think_) |
| User | think_user |
| UserType | think_user_type |
如果你的规则和上面的系统约定不符合,那么需要设置 Model 类的数据表名称属性,以确保能够找到对应的数据表。
模型自动对应的数据表名称都是遵循小写+下划线规范,如果你的表名有大写的情况,必须通过设置模型的 table 属性。
如果希望给模型类添加后缀,也就是说模型类名称和表名不一样的情况,则需要设置 name 属性或者 table 属性。例如:将上面示例 User 模型类名更改为 UserModel:
php
<?php
namespace app\model;
use think\model;
class UserModel extends Model {
// 模型名
protected $name = 'user';
}
2、模型设置
模型默认主键为 id,如果没有使用 id 作为主键名,则需要在模型中通过设置 pk 属性来指定主键:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 主键
protected $pk = 'uid';
}
你也可以使用 table 属性指定模型对应数据库表,connection 属性指定数据库连接。例如:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 设置当前模型对应的完整数据表名称
protected $table = 'think_user';
// 设置当前模型的数据库连接
protected $connection = 'db_config';
}
connection 属性使用配置参数名(需要在数据库配置文件中的 connections 参数中添加对应标识)。
常用的模型设置属性包括(以下属性都不是必须设置):
|------------|-------------------------------|
| 属性 | 描述 |
| name | 模型名(相当于不带数据表前后缀的表名,默认为当前模型类名) |
| table | 数据表名(默认自动获取) |
| suffix | 数据表后缀(默认为空) |
| pk | 主键名(默认为id) |
| autoInc | 数据表自增主键 支持字符串或true(自动获取主键值) |
| connection | 数据库连接(默认读取数据库配置) |
| query | 模型使用的查询类名称 |
| field | 模型允许写入的字段列表(数组) |
| schema | 模型对应数据表字段及类型 |
| type | 模型需要自动转换的字段及类型 |
| strict | 是否严格区分字段大小写(默认为true) |
| disuse | 数据表废弃字段(数组) |
注意:name 和 table 属性如果同时定义则 table 属性优先。
3、模型操作
在模型中除了可以调用数据库类的方法之外(换句话说,数据库的所有查询构造器方法模型中都可以支持),可以定义自己的方法,所以也可以把模型看成是数据库的增强版。
模型的操作方法无需和数据库查询一样调用必须首先调用 table 方法,因为模型会按照规则自动匹配对应的数据表。例如:
php
Db::table('user')->where('id', '>', 10)->select();
改成模型操作的话就变成
php
User::where('id', '>', 10)->select();
虽然看起来是相同的查询条件,但一个最明显的区别是查询结果的类型不同。第一种方式的查询结果是一个(二维)数组,而第二种方式的查询结果是包含了模型(集合)的数据集。不过,在大多数情况下,这二种返回类型的使用方式并无明显区别。
模型操作和数据库操作的另外一个显著区别是模型支持包括获取器、修改器、自动时间写入在内的一系列自动化操作和事件,简化了数据的存取操作,但随之而来的是性能有所下降(其实并没下降,而是自动帮你处理了一些原本需要手动处理的操作)。
二、模型字段
模型的数据字段和对应数据表的字段是对应的,默认会自动获取(包括字段类型),但自动获取会导致增加一次查询(可以开启字段缓存功能),因此可以在模型中明确定义字段信息避免多一次查询的开销。
示例
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 设置字段信息
protected $schema = [
'id' => 'int',
'name' => 'string',
'age' => 'int',
'status' => 'int'
];
}
字段类型的定义可以使用PHP类型或者数据库的字段类型都可以,字段类型定义的作用主要用于查询的参数自动绑定类型。
时间字段尽量采用实际的数据库类型定义,便于时间查询的字段自动识别。如果是 json 类型直接定义为 json 即可。
1、字段类型
schema 属性一旦定义,就必须定义完整的数据表字段类型。如果只希望对某个字段定义需要自动转换的类型,可以使用 type 属性。例如:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 设置字段自动转换类型
protected $type = [
'name' => 'string'
];
}
2、废弃字段
如果因为历史遗留问题 ,数据表存在很多的废弃字段,可以在模型里面定义 disuse 属性来废弃不再使用的字段。
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 设置废弃字段
protected $disuse = ['start', 'end'];
}
在查询和写入的时候会忽略定义的 start 和 end 废弃字段。
3、获取数据
在模型外部获取数据的方法如下:
php
$user = User::find(1);
echo $user->name;
echo $user->age;
由于模型类实现了 ArrayAccess 接口,所以也可以当成数组使用。
php
$user = User::find(1);
echo $user['name'];
echo $user['age'];
获取数据的方法不要在模型内部实现而应该放入逻辑层或控制器层,如果你必须在模型内部获取数据的话,需要改成:
php
$user = $this->find(1);
echo $user->getAttr('name');
echo $user->getAttr('age');
否则可能会出现意想不到的错误。
4、模型赋值
可以使用下面的代码给模型对象赋值
php
$user = new User();
$user->name = 'zhangsan';
$user->age = 25;
该方式赋值会自动执行模型的修改器,如果不希望执行修改器操作,可以使用
php
$data['name'] = 'zhangsan';
$data['age'] = 25;
$user = new User($data);
或者使用
php
$user = new User();
$data['name'] = 'zhangsan';
$data['age'] = 25;
$user->data($data);
data 方法支持使用修改器,只需要在第二个参数传入 true 即可
php
$user = new User();
$data['name'] = 'zhangsan';
$data['age'] = 25;
$user->data($data, true);
如果需要对数据进行过滤,可以使用第三个参数,传入一个数组,指定需要使用的数据
php
$user = new User();
$data['name'] = 'zhangsan';
$data['age'] = 25;
$user->data($data, true, ['name', 'age']);
表示只设置 data 数组的 name 和 age 数据。
注意 :data 方法会清空调用前设置的数据。
5、严格区分字段大小写
默认情况下,模型数据名称和数据表字段应该保持严格一致,也就是说区分大小写。例如:
php
$user = User::find(1);
echo $user->create_time; // 正确
echo $user->createTime; // 错误
注意:严格区分字段大小写的情况下,如果你的数据表字段是大写,模型获取的时候也必须使用大写。
如果希望在获取模型数据的时候不区分大小写(前提是数据表的字段命名必须规范,即小写+下划线),可以设置模型的 strict 属性为 false。
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 模型数据不区分大小写
protected $strict = false;
}
现在可以使用
php
$user = User::find(1);
// 下面两种方式都有效
echo $user->createTime;
echo $user->create_time;
6、模型数据转驼峰
可以设置 convertNameToCamel 属性为 true,使得模型数据返回驼峰方式命名(前提也是数据表的字段命名必须规范,即小写+下划线)。
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 数据转换为驼峰命名
protected $convertNameToCamel = true;
}
然后在模型输出的时候可以直接使用驼峰命名的方式获取。
php
$user = User::find(1);
$data = $user->toArray();
echo $data['createTime']; // 正确
echo $user['create_time']; // 错误
三、新增
模型数据的新增和数据库的新增数据有所区别,数据库的新增只是单纯的写入给定的数据,而模型的数据写入会包含修改器、自动完成以及模型事件等环节。
1、添加一条数据
第一种方式是实例化模型对象后赋值并保存:
php
$user = new User;
$user->name = '测试';
$user->age = 22;
$user->save(); // 调用 save() 方法保存数据
save 方法成功会返回 true,并且只有当 before_insert 事件返回 false 的时候返回 false,一旦有错误就会抛出异常。所以无需判断返回类型。
第二种方式是直接传入数据到 save 方法批量赋值:
php
$user = new User;
$user->save([
'name' => '测试',
'age' => 22
]);
save 方法支持传入模型实例或实体对象实例。
默认只会写入数据表已有的字段,如果通过外部提交赋值给模型,并且希望指定某些字段写入,可以使用 allowField 方法指定写入字段:
php
$user = new User;
// post数组中只有 name 和 age 字段会写入
$user->allowField(['name','age'])->save($_POST);
最佳的建议是模型数据赋值之前就进行数据过滤,例如:
php
$user = new User;
// 过滤 post 数组中的非数据表字段数据
$data = Request::only(['name','age']);
$user->save($data);
2、获取自增ID
如果要获取新增数据的自增ID,可以使用下面的方式:
php
$user = new User;
$user->save([
'name' => '测试',
'age' => 22
]);
// 获取自增ID,保存成功后可以直接通过id字段获取当前自增id
echo $user->id;
这里其实是获取模型的主键,如果主键不是id,而是 user_id 的话,获取自增ID就需要根据实际的主键名称进行更改。
如果希望自动获取主键值,也可以使用 getKey 方法,此时就不需要关注主键的名称了。
php
$user = new User;
$user->save([
'name' => '测试',
'age' => 22
]);
// 获取自增ID
echo $user->getKey();
不要在同一个实例里面多次新增数据,如果确实需要多次新增,可以使用后面介绍的静态方法处理。
3、数据自动写入
如果需要在创建数据的时候自动写入相关字段,可以通过定义 insert 属性并配合修改器来完成。
示例 自动写入订单编号
php
<?php
namespace app\model;
use think\model;
class Order extends Model
{
protected $insert = ['order_no']; // 设置自动写入的字段
protected $readonly = ['order_no'];
protected function setOrderNoAttr($value, $data)
{
return date('YmdHis') . mt_rand(1000, 9999);
}
}
创建订单会自动写入订单编号字段 order_no,自动写入的前提是必须有定义修改器方法或定义了类型转换接口实现。同时这里还设置了只读字段,更新的时候不会写入。
如果需要自动写入一个固定的值,可以使用下面的定义:
php
<?php
namespace app\model;
use think\Model;
class Order extends Model
{
protected $insert = ['status' => 1];
}
在新增数据的时候会自动写入 status 为1的状态值。
4、批量增加数据
批量新增数据,可以使用 saveAll 方法:
php
$user = new User;
$list = [
['name' => '测试1', 'age' => 18],
['name' => '测试2', 'age' => 20]
];
$user->saveAll($list);
saveAll 方法新增数据返回的是包含新增模型(带自增ID)的数据集对象。
saveAll 方法新增数据默认会自动识别数据是需要新增还是更新操作,当数据中存在主键的时候会认为是更新操作。
5、静态方法
可以直接静态调用 create 方法创建并写入数据:
php
$user = User::create([
'name' => '测试',
'age' => 20
]);
echo $user->id; // 获取自增ID
和 save 方法不同的是,create 方法返回的是当前模型的对象实例。
create 方法默认会过滤不是数据表的字段信息,可以在第二个参数传入允许写入的字段列表,例如:
php
// 只允许写入 name 和 age 字段的数据
$user = User::create([
'name' => '测试',
'age' => 20
], ['name', 'age']);
echo $user->id; // 获取自增ID
新增数据的最佳实践原则:使用 create 方法新增数据,使用 saveAll 方法批量新增数据。
四、更新
和模型新增一样,更新操作同样也会经过修改器、自动完成以及模型事件等处理。更新方法和新增方法使用的是同一个方法,通常系统会自动判断需要新增还是更新数据。
1、查找并更新
在获取数据后,更改字段内容然后使用 save 方法更新数据。这种方式是最佳的更新方式。
php
$user = User::find(1); // 查询主键为 1 的数据
// 更改需要需要更新字段的内容
$user->name = '张三';
$user->age = 28;
$user->save(); // 调用 save 方法更新数据
save 方法也支持传入数组进行数据更新:
php
$user = User::find(1); // 查询主键为 1 的数据
$user->save([
'name' => '张三',
'age' => 28
]);
save 方法更新数据,只会更新变化的数据,对于没有变化的数据是不会进行重新更新的。如果需要强制更新数据,可以使用 force 方法:
php
$user = User::find(1);
$user->name = '张三';
$user->age = 28;
$user->force()->save();
这样无论修改后的数据是否和之前一样都会强制更新该字段的值。
如果要执行SQL函数更新,可以使用下面提供的方法:
php
$user = User::find(1);
$user->name = '张三';
$user->age = Db::raw('age + 2');
$user->save();
2、批量更新数据
使用 saveAll 方法可以批量更新数据,只需要在批量更新的数据中包含主键即可。例如:
php
$user = new User;
$list = [
['id' => 1, 'name' => '张三', 'age' => 18],
['id' => 2, 'name' => '李四', 'age' => 20]
];
$user->saveAll($list);
批量更新方法返回的是一个数据集对象。
批量更新仅能根据主键值进行更新,其它情况需自行处理。
3、静态方法
可以直接静态调用 update 方法更新数据:
php
User::update(['name' => 'zhangsan'], ['id' => 1]);
模型的 update 方法返回模型的对象实例
如果第一个参数中已经包含主键数据,则无需传入第二个参数(更新条件)。例如:
php
User::update(['name' => 'zhangsan', 'id' => 1]);
如果你需要只允许更新指定字段,可以传入第三个参数,用于指定更新字段:
php
User::update(['name' => '张三', 'age' => 18], ['id' => 1], ['name']);
上面的代码只会更新 name 字段的数据。
4、自动识别
我们已经看到,模型的新增和更新方法都是 save 方法,系统有一套默认的规则来识别当前的数据需要更新还是新增。
- 实例化模型后调用 save 方法表示新增
- 查询数据后调用 save 方法表示更新
不要在一个模型实例里面做多次更新,会导致部分重复数据不再更新,正确的方式应该是先查询后更新或者使用模型类的 update 方法更新。
五、删除
模型的删除和数据库的删除方法区别在于,模型的删除会包含模型的事件处理。
1、删除当前模型
删除模型数据,可以在查询后调用 delete 方法。
示例
php
$user = User::find(1);
$user->delete();
delete 方法返回布尔值。
2、根据主键删除
直接静态调用 destroy 方法删除数据(根据主键删除):
php
User::destroy(1);
// 支持批量删除多条数据
User::destroy([1,2,3]);
当 destroy 方法传入空值(包括空字符串和空数组)的时候不会做任何的数据删除操作。
3、条件删除
destroy 方法还支持使用闭包删除,我们可以在闭包中指定数据删除的条件。例如:
php
User::destroy(function($query){
$query->where('age', '>', 18);
});
或者通过数据库类的查询条件删除:
php
User::where('age', '>', 18)->delete();
直接调用数据库 delete 方法的话,则无法调用模型事件。
六、查询
模型查询和数据库查询方法的区别主要在于,模型中查询的数据在获取的时候会经过获取器的处理,以及更加对象化的获取方式。模型查询除了使用自身的查询方法外,也可以使用数据库的查询构造器,返回的都是模型对象实例。
1、获取单个数据
模型使用 find 方法获取单个数据,或者使用查询构造器查询满足条件的数据。
示例
php
// 查询主键为 1 的数据
$user = User::find(1);
echo $user->name;
// 使用查询构造器查询满足条件的数据
$user = User::where('name', 'zhangsan')->find();
echo $user->name;
模型使用 find 方法查询,如果数据不存在返回 Null,否则返回当前模型的对象实例。如果希望查询数据不存在则返回一个空模型,可以使用 findOrEmpty 方法。
可以用 isEmpty 方法来判断当前是否为一个空模型。
示例
php
$user = User::where('name', 'zhangsan')->findOrEmpty();
if (!$user->isEmpty()) {
echo $user->name;
}
find 方法可以传入闭包,当没有查询到数据的时候会执行闭包返回。
php
User::where('id', 1)->find(function(Query $query){
// 执行其它查询操作并返回
// return $query->where()->find();
});
2、获取多个数据
使用 select 方法可以一次获取多个数据,同 find 方法一样,select 方法也可以使用查询构造器。
示例
php
// 根据主键获取多个数据
$list = User::select([1, 2, 3]);
// 对数据集进行遍历操作
foreach($list as $key=>$user){
echo $user->name;
}
3、使用查询构造器
在模型中仍然可以调用数据库的链式操作和查询方法,可以充分利用数据库查询构造器的优势。
例如:
php
User::where('id', 1)->find();
User::where('status', 1)->order('id desc')->select();
User::where('status', 1)->limit(10)->select();
使用查询构造器直接使用静态方法调用即可,无需先实例化模型。
value 方法可以获取某个字段的值,而 column 方法可以获取某列的值。
示例
php
// 获取某个字段的值
User::where('id', 1)->value('age');
// 获取某个列的所有值
User::where('status', 1)->column('name')
value 和 column 方法返回的不再是一个模型对象实例,而是纯粹的值或者某个列的数组。
3.1、动态查询
模型也支持数据库的动态查询方法。例如:
php
// 根据指定字段查询数据
$user = User::getByName('zhangsan');
3.2、聚合查询
同样在模型中也可以调用数据库的聚合方法查询。例如:
php
User::count();
User::where('status', '>', 0)->count();
User::where('status', 1)->avg('age');
注意,如果字段不是数字类型,在使用 max / min 的时候,需要加上第二个参数。
php
User::max('name', false);
3.3、数据分批处理
模型也可以支持对返回的数据进行分批处理,这在处理大量数据的时候非常有用。例如:
php
// 每批处理 100 条数据
User::chunk(100, function ($users) {
foreach($users as $user){
// 处理 user 模型对象
}
});
4、使用游标查询
模型也可以使用数据库的 cursor 方法进行游标查询,返回生成器对象。
示例
php
foreach(User::where('status', 1)->cursor() as $user){
echo $user->name;
}
七、查询范围
可以对模型的查询和写入操作进行封装,例如:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function scopeZhangsan($query)
{
$query->where('name', 'zhangsan')->field('id,name');
}
public function scopeAge($query)
{
$query->where('age', '>', 18)->limit(20);
}
}
然后就可以进行下面的条件查询:
php
// 查询 name 为 zhangsan 的数据
User::scope('zhangsan')->find();
// 查找年龄大于 18 的 20 条数据
User::scope('age')->select();
// 查找 name 为 zhangsan 并且 age 大于 18 的 20 条数据
User::scope('zhangsan,age')->select();
查询范围的方法可以定义额外的参数,例如 User 模型类定义如下:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function scopeUsername($query, $name)
{
$query->where('name', 'like', '%' . $name . '%');
}
public function scopeAge($query, $age)
{
$query->where('age', '>', $age)->limit(20);
}
}
在查询的时候可以如下使用:
php
// 查询 name 包含 zhangsan 并且 age 大于 18 的 20 条数据
User::username('zhangsan')->age(18)->select();
可以直接使用闭包函数进行查询,例如:
php
User::scope(function($query){
$query->where('age', '>', 18)->limit(20);
})->select();
注意:查询范围只能支持 find 或者 select 查询。
1、全局查询范围
支持在模型里面设置 globalScope 属性,用来定义全局的查询范围。
示例
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 定义全局的查询范围
protected $globalScope = ['status'];
public function scopeStatus($query)
{
$query->where('status', 1);
}
}
然后,执行查询的时候,就会自动加上全局查询范围的条件:
php
User::find(1);
// 生成的SQL语句为:SELECT * FROM `user` WHERE `id` = 1 AND `status` = 1 LIMIT 1
// 从生成的SQL语句中可以看到,加上了全局查询范围的条件
可以使用 withoutGlobalScope 方法动态关闭部分或所有的全局查询范围。例如:
php
// 关闭 status 全局查询范围
User::withoutGlobalScope(['status'])->select();
// 关闭所有全局查询范围
User::withoutGlobalScope()->select();