Thinkphp 第五章:模型和对象

从本章开始就要揭开模型的神秘面纱了,本章主要学习模型的定义和基础使用,以及和数据库操作的区别,学习内容主要包含:

模型和数据库区别

在说模型和数据库的区别之前,首先理解一点,5.0的数据库抽象访问层(我们后面用Db类表示)和模型是一个整体,共同完成了ThinkPHP5.0ORM(对象关系映射)。或者也可以理解为模型是数据访问层的查询构造器延伸,完成更高级的数据库查询操作罢了。

通过前面几章的学习,看起来Db类已经非常的强大,但缺点仍然非常明显:

  • 不支持ActiveRecord实现;
  • 缺乏灵活的事件机制;
  • 数据自动处理能力弱;
  • 数据关联操作繁琐并且不直观;
  • 不能单独封装业务逻辑;

上面这些内容我只是打击下迷恋Db类的朋友,别无它意(因为本来就是故意设计的_)。

其实还有很多...当然,原因并不是否定Db类的实现,而是前面提到的,Db和模型本来就是一个整体,只是各自的职责和分工不同,如果没有Db类的基石,模型也只是建在沙滩上的城堡罢了。

Db和模型的存在只是ThinkPHP5.0架构设计中的职责和定位不同,Db负责的只是数据(表)访问,模型负责的是业务数据和业务逻辑。

当然,模型层可以分的更细,把数据模型和逻辑模型,甚至服务模型分开,这个暂时就不在目前的讨论范畴了,只不过把模型层的职责和分工更细化。

如果你用框架只是用来管理一些数据的CURD而没有业务需要(其实本质上来说任何的系统都是CURD,业务逻辑都是抽象和封装出来的,这是设计层面的问题了),那么也许看起来Db类已经够用了(你不觉得其实数据库本身已经可以完成了么),但是作为一个业务系统或者平台(无论是WEB还是API),通常每个数据表就对应了一个业务模型对象,甚至存在和其它业务模型的混合和关联逻辑。举个用户表的例子,用户登录这样一个业务逻辑其实包含了很多的关联操作,你得检查用户账号是否正常,用户名和密码是否正确,然后记录用户的最后登录时间和IP(如果IP所在区域不符有些系统还需要给用户发邮件提醒),还要给用户增加积分,甚至可能还需要检查用户的权限,那么Db类就显得吃力了,这其实也是数组存储结构和对象存储设计的差异,业务越复杂,这种差异越明显,PHP的数组再强大也替代不了对象。

Db和模型最明显的一个区别就是Db查询返回的数据类型为数组(对于一个没有业务逻辑的数据而言,数组已经足够),而模型的查询返回类型的是模型对象实例。

也许前面几个问题你根本不会在意(确实优雅只是看起来舒服一些罢了,关联用JOIN还容易掌控之类的话我也经常听到,呵呵~),但最后一个问题无法封装业务逻辑是致命的,处理不当极易出现违反MVC架构设计的混乱情况。

再说简单一点,由于Db类的数据操作并没有一个唯一对应的对象实例,也就无法封装业务方法,就变成你的业务方法要么写到控制器方法里面,要么定义到另外一个所谓的“业务逻辑”层里面,前者显然是违反MVC架构设计思想的,而后者其实就是一个模型类的概念存在了,那么是否需要拥抱模型就显而易见,不用我多说了吧_

话说回来了,有些人虽然用了模型,但仍然在模型里面大量封装直接操作Db类的代码和方法,这也是一种伪模型设计,并不可取。

比较Db和模型,不要单纯从功能上做比较,这是次要的,也没意义,毕竟职责定位不同。也不要在意性能上的差异,这个对于业务逻辑来说,一次查询就抵消了。

总而言之,想要掌握模型,必须明白和理解下面几个原则:

  • 模型和数据库层的定位和职责不同;
  • 不要因为性能而放弃使用模型,那是得不偿失的;
  • 用面向对象的方式来使用和设计模型;
  • 模型的数据底层操作仍然是数据库抽象访问层,而且是自动的;

模型设计基于数据访问层之上,并作了更高层次的封装,实现了Db类本身不支持的功能,或者简化了原本使用Db类的复杂操作。从查询操作的角度来看,可以理解为Db类是数据表的查询构造器,而模型是业务模型的查询构造器,其实都属于查询构造器的范畴。

很多人不习惯用模型的原因无非就几个方面:

  • 不理解模型的概念;
  • 嫌每个数据表都要定义模型麻烦;
  • 模型的用法不容易掌握;
  • 觉得模型的性能差;

我们会慢慢打消上述的这些困惑或顾虑,学完本书,你就会发现模型其实很简单,而且相对于Db查询来说牺牲的细微性能完全值得。

在控制器中永远调用的是模型类,然后在模型类中封装业务逻辑方法和数据处理,完成业务操作。对控制器来说,模型就是一个业务逻辑接口,并且善于运用依赖注入机制来绑定模型对业务操作会带来极大的便利。

模型定义

定义一个模型很简单,下面是一个最简单的模型类:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
}

如果使用的是5.1版本,一定要注意,如果你的对应数据表的主键名不是id,需要在模型中设置pk属性为你的实际主键名。

模型定义有几个要素:

  • 通常会继承think\Model(或者子类),虚拟模型除外;
  • 一个模型并不总是对应一个数据表(可能会有多个),虽然默认如此;
  • 模型名和数据表名也不是直接对应关系;
  • 尽管一个空模型和使用Db类无异,但意义不同;

模型定义阶段要达成的目的:

  • 定义数据表(默认就是模型类名)
  • 定义数据表主键(默认会自动获取)
  • 定义数据库连接(默认使用数据库配置)
  • 定义数据处理逻辑(包括属性和方法)
  • 定义业务逻辑(方法)

下面的定义是不需要或者不支持的:

  • 数据表字段(不需要,会自动获取,并支持缓存机制)
  • 数据表前缀(不支持,模型不关心前缀)

大多数情况下,数据表和数据库连接是不需要定义的,数据处理逻辑和业务逻辑才是模型定义的重点,如果你发现你的大多数模型类都是什么都没定义,那么就要思考下哪里出问题了,为什么你的模型成了形式和摆设。是没业务需要还是职责分工有问题了?也许你在控制器中大量使用Db类进行业务逻辑处理。无论怎样,现在纠正思维,跟着教程拥抱和学习模型吧。

一个模型并不总是对应一个数据表(例如关联模型和聚合模型),但大多数情况下对应的是一个数据表,默认的对应关系是:模型类的名称(注意不一定是类名,后面会解释)转换为小写和下划线就是对应的数据表:

模型名 对应数据表
User user
UserType user_type

如果你的数据库配置定义了前缀(假设数据库的前缀定义是 think_),那么对应关系就是:

模型名 对应数据表
User think_user
UserType think_user_type

如果你的对应规则和上面的系统约定不符合,那么需要设置模型类的数据表名称属性,以确保能够找到对应的数据表。代码如下:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $table = 'user_info';
}

table属性定义的是完整数据表名,如果你希望定义不带前缀的数据表名,可以使用name属性来定义模型的名称。

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $name = 'user_info';
}

如果你同时定义了这两个属性,那么table属性是优先的。

模型的设计允许给单独指定数据库连接,也就说你可以将不同的数据库的表进行统一的管理,对于跨数据库的应用尤其有用,对于跨库的相同表名,我们可以建立不同名称的模型或者放入不同的命名空间来解决。

指定模型的单独数据库连接方法如下:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $name       = 'user_info';
    protected $connection = 'db_config';
}

Db类的connect方法一样,模型类的connection属性允许使用数组、字符串以及配置参数的方式定义,这里使用配置参数(在应用或者模块的配置文件中单独配置db_config参数)的方式,避免在模型里面写死数据库连接信息,全部交给配置文件去统一处理。

如果connection属性使用数组方式配置,会和数据库配置文件中的参数合并,因此你只需要定义有区别的参数,而无需定义全部的数据库参数。

如果担心模型的名称和PHP关键字冲突,可以启用类后缀功能,只需要在应用配置文件中设置:

    // 开启应用类库后缀
    'class_suffix'           => true,

开启后,所有的应用类库定义的时候都需要加上对应后缀,包括控制器类。

这样app\index\model\User类定义就要改成

<?php

namespace app\index\model;

use think\Model;

class UserModel extends Model
{
}

并且类名也要改为UserModel.php

关于模型的连接对象和查询对象,要清楚下面这些事实:

  • 模型可以单独设置数据库连接;
  • 模型的数据库连接是惰性的(因为连接本身就是惰性);
  • 如果使用统一的数据库配置,模型使用的连接对象是相同的;
  • 模型使用的查询对象是独立的;
  • 模型可以使用自定义的查询对象;

命令行生成

当你需要创建大量的模型类的时候,不妨考虑下命令行生成,可以快速创建模型类。

在windows下面,使用Win+R输入cmd进入命令控制台,切换到项目根目录(也就是think文件所在目录),并执行下面的指令可以生成index模块的Blog模型类文件。

>php think make:model index/Blog

生成的模型类文件如下:

<?php

namespace app\index\model;

use think\Model;

class Blog extends Model
{
    //
}

注意,如果使用

>php think make:model Blog

生成的是common模块下面的Blog模型类。

模型调用

模型支持实例化调用和静态调用(主要是查询,查询后会返回一个模型对象实例)。

// 实例化User模型
$user = new \app\index\model\User();
// 直接静态查询
$user = \app\index\model\User::get(1);

一般来说,我们会事先使用use引入User模型类,就不需要每次都使用完整命名空间方式来调用User模型类了。

<?php

namespace app\index\controller;

use app\index\model\User;

class Index
{
    public function index()
    {
        $user = User::get(1);
    }
}

如果你开启了应用类库后缀的话,可以这样使用

<?php

namespace app\index\controller;

use app\index\model\UserModel as User;

class IndexController
{
    public function index()
    {
        $user = User::get(1);
    }
}

我们后面的例子都是直接使用User类名进行实例化或者静态调用,你必须明白为何可以如此调用。

调用模型类的方法其实和调用一个普通的类没有区别,不要觉得模型类有什么特殊。

如果你觉得每次引入比较麻烦,系统还提供了一个助手函数帮助你快速实例化模型类而不必每次引入模型类。

你可以在任何地方使用

$user = model('User');

实例化User模型类,并且model函数采用单例实现,多次调用不会重复实例化。

使用model助手函数的一个优势是即使你开启了应用类库后缀,你仍然可以直接使用

$user = model('User');

而不必使用

$user = model('UserModel');

事实上,上面的用法是错误的。

我们还是建议使用use方式引入模型类后操作,因为助手函数并不支持模型的静态调用,这个后面我们还会详细说明。

我们甚至可以通过依赖注入直接把模型对象实例注入到控制器的操作方法中,而不需要每次都进行实例化。关于如何使用依赖注入,请参考《控制器从入门到精通》第五讲的内容。

对象化操作

了解如何定义和调用模型后,我们来具体了解下模型的使用。

模型和Db操作的一大显性区别就是一个是对象操作和一个是数组操作,下面以一个user数据表的查询、取值、设置和更新的例子,来说明下两种方式的区别。

首先回顾下Db类的用法:

// 查询操作
$user = Db::table('user')->find(1);

// 取值操作
echo $user['name'];
echo $user['email'];

// 设置操作
$user['name']  = 'topthink';
$user['email'] = 'thinkphp@qq.com';

// 更新操作
Db::table('user')->update($user);

然后,如果是模型操作的话,就可以对应下面的代码实现:

// 查询操作
$user = User::get(1);

// 取值操作
echo $user->name;
echo $user->email;

// 设置操作
$user->name  = 'topthink';
$user->email = 'thinkphp@qq.com';

// 更新操作
$user->save();

事实上,由于模型类实现了ArrayAccess接口,因此一样可以使用数组方式操作:

// 查询操作
$user = User::get(1);

// 取值操作
echo $user['name'];
echo $user['email'];

// 设置操作
$user['name']  = 'topthink';
$user['email'] = 'thinkphp@qq.com';

// 更新操作
$user->save();

是不是觉得很神奇,不过这个问题有点高级,暂且不表,留给大家思考,答案后面章节会揭晓。我们后面的模型例子还是以对象操作为例讲解。

模型对象的取值和设置都不是表面上看起来那么简单,可以设置很多自动化操作,取值的自动化操作就是读取器,设置的自动化操作就是修改器,这两个概念我们会在下一章详细讲解。

模型的读取和设置并不总是这样操作,这和模型的内部实现有关,因为我们并没有在模型里面定义user数据表对应的public类型的name或者email属性,模型的取值和设置都是通过__get__set魔术方法完成,事实上模型的数据操作内部都是操作模型类的data属性,在本书中,我们通常把$user->name$user->email称为模型数据而不是模型属性

那么问题来了,如果是在模型内部进行取值和设置操作怎么办?

// 错误的读取数据方式
echo $this->name;
echo $this->email;
// 错误的数据设置方式
$this->name = 'thinkphp';
$this->email = 'thinkphp@qq.com';

这样,一旦数据表的字段名和模型的内部属性冲突就产生混淆了,这是一个新手最容易产生困惑的地方。所以,如果是在模型内部,正确的获取方式应该是:

// 模型内部读取数据
echo $this->getData('name');
echo $this->getAttr('email');
// 模型内部设置数据
$this->data('name','thinkphp');
$this->setAttr('email','thinkphp@qq.com');

name属性为例,获取模型数据的方式有下列三种:

场景 方法
外部获取模型数据 $model->name
内部获取模型数据 $this->getAttr('name')
内部获取(原始)模型数据 $this->getData('name')

getDatagetAttr方法的区别前者是原始数据,后者是经过读取器处理的数据,如果没有定义数据读取器的话,两个方法的结果是相同的。

对应的设置模型数据的方式也有三种:

场景 方法
外部设置模型数据 $model->name='thinkphp'
内部设置模型数据(经过修改器) $this->setAttr('name','thinkphp')
内部设置模型数据 $this->data('name','thinkphp')

datasetAttr方法的区别前者是赋值最终数据,后者赋值的数据还会经过修改器处理,如果没有定义修改器的话,两个方法的结果是相同的。

对象化操作的神奇是可以级联读取或者设置,例如:

// 查询操作
$user = User::get(1);

// 取值操作
echo $user->name;
echo $user->email;

// 关联取值
echo $user->role->name;
echo $user->contact->phone;

// 设置操作
$user->name  = 'topthink';
$user->email = 'thinkphp@qq.com';

// 更新操作
$user->save();

// 关联设置
$user->role->name = 'admin';
$user->role->save();

$user->contact->phone = '123456789';
$user->contact->save();

这里使用了模型关联的概念,如果感到摸不着头脑不用担心,我们会在第八章给你详细讲解。

模型CURD操作

模型的主要功能包括数据处理和业务逻辑,而这些都离不开数据的CURD操作,因此我们首先来谈下数据的CURD操作,在掌握了数据库Db类的用法后,模型的CURD操作就会很容易理解,因为本质上模型的CURD操作最终调用的还是Db类的操作,区别在于使用了ActiveRecord模式和单独做了一层封装而已,我们来看下两种方式CURD操作用法的简单对比(其中模型会给出动态和静态两种实现方法,分别对应不同的场景)。

创建Create

Db用法:

Db::table('user')
    ->insert([
        'name'  => 'thinkphp',
        'email' => 'thinkphp@qq.com',
    ]);

模型用法:

$user        = new User;
$user->name  = 'thinkphp';
$user->email = 'thinkphp@qq.com';
$user->save();

或者批量设置:

$user = new User;
$user->save([
    'name'  => 'thinkphp',
    'email' => 'thinkphp@qq.com',
]);

上面两种方式等效,当你的模型数据比较多不想一一赋值的时候,可以使用后者。

也许你咋一看还觉得麻烦了,又是实例化又是赋值的,但好处多多,慢慢你就会体会到了,看起来是一个简单的赋值和保存操作其实内里大有乾坤,可以触发很多处理甚至事件。

save方法的返回值不是自增主键的值(和Db的execute方法一样返回影响的记录数),要获取自增主键的值可以使用下面的方式:

$user        = new User;
$user->name  = 'thinkphp';
$user->email = 'thinkphp@qq.com';
$user->save();
// 获取用户的主键数据
echo $user->id;

可以使用静态方法创建数据

$user = User::create([
    'name'  => 'thinkphp',
    'email' => 'thinkphp@qq.com',
]);
echo $user->id;

save方法不同,create方法的返回值是User模型的对象实例,而save方法调用的时候本身就在对象实例里面。

很多开发者不习惯静态调用,这里必须说明的是模型类的静态CURD操作其实都是内部自动实例化而已,所以说白了提供的这些静态操作方法只是对动态CURD操作方法的静态封装罢了。

至于静态方法的场景,主要是不想实例化或者不方便实例化的需求,而且支持变量的静态调用,例如:

$model = '\app\index\model\User';
$user  = $model::create([
    'name'  => 'thinkphp',
    'email' => 'thinkphp@qq.com',
]);

创建操作用法小结:

方法 返回值
save(动态) 影响的记录数
create(静态) 模型对象实例

读取Read

Db类实现读取单个记录

$user = Db::table('user')
    ->where('id', 1)
    ->find();
//  或者
$user = Db::table('user')
    ->find(1);
echo $user['id'];
echo $user['name'];

模型实现读取单个记录要比Db类简单很多,而且更加符合对象的设计。

$user = User::get(1);
echo $user->id;
echo $user->name;

V5.0.8版本之前模型的get方法如果没有传值或者传入空值,会查询第一个符合条件的数据,这个问题在V5.0.8版本已经修正,get方法必须传入非空的值,否则直接返回Null。

Db类的find方法返回的是一个数组,模型类的get方法返回的是一个User模型对象实例。模型的读取操作一般使用静态方法读取即可,返回模型对象实例。

很多用户往往会写出下面的代码,理论上来说当然也没有错,其实是大可不必的。

$user = new User;
$user->find(1);

除非你已经在User模型的对象实例内部去调用find方法读取数据,但这种方式不符合模型对象的设计原则,一个模型对象实例应该唯一对应数据表的一条记录

Db类实现读取多个记录

// 查询用户数据集
$users = Db::table('user')
    ->where('id', '>', 1)
    ->limit(5)
    ->select();

// 遍历读取用户数据
foreach ($users as $user) {
    echo $user['id'];
    echo $user['name'];
}

模型实现读取多个记录

// 查询用户数据集
$users = User::where('id', '>', 1)
    ->limit(5)
    ->select();

// 遍历读取用户数据
foreach ($users as $user) {
    echo $user->id;
    echo $user->name;
}

模型的查询操作比起Db查询有一个显著的特征就是不需要每次调用table或者name方法,因为每个模型在创建的时候已经自动对应了数据表。

在读取多个记录的方式上,两种方式的区别并不大,只是默认返回数据集类型的区别,Db方式返回的数据集是一个包含每个用户数组的二维数组,而模型方式返回的数据集包含每个User模型对象实例的数组。

事实上,这个差异在实际进行数据集处理的时候根本感觉不到,也就是说后者仍然可以使用前者的方式统一操作(这归功于模型的神奇设计,这个后面章节会专门提到)。

对于多个主键的数据读取,模型还封装了一个all方法,用法如下:

// 查询用户数据集
// 相当于 Db::table('user')->select([1,2,3]);
$users = User::all([1, 2, 3]);

// 遍历读取用户数据
foreach ($users as $user) {
    echo $user->id;
    echo $user->name;
}

关于模型的getall方法的更多用法,而且也完全可以替代数据库提供的findselect方法,我们会在模型高级用法一章中给你继续深入。

其实对于读取数据的操作,模型提供了很强大的处理机制,为了避免你初期的时候混淆,我们暂且略过,会在以后专门讲解。

读取操作用法小结:

原则上模型的查询都应该是静态调用

方法 作用 返回值
get 查询单个记录 模型对象实例
find 查询单个记录 模型对象实例
all 根据主键查询多个记录 包含模型对象实例的数组或者数据集
select 根据条件查询多个记录 包含模型对象实例的数组或者数据集

更新Update

Db类实现

Db::table('user')
    ->where('id', 1)
    ->update([
        'name'  => 'topthink',
        'email' => 'topthink@qq.com',
    ]);

模型实现

$user        = User::get(1);
$user->name  = 'topthink';
$user->email = 'topthink@qq.com';
$user->save();

或者使用

$user = User::get(1);
$user->save([
    'name'  => 'topthink',
    'email' => 'topthink@qq.com',
]);

静态调用

User::update([
    'name'  => 'topthink',
    'email' => 'topthink@qq.com',
], ['id' => 1]);

save方法返回影响的记录数,而update方法返回的则是模型的对象实例。

模型和Db更新方法的最大区别是模型的更新方法只会更新有变化的数据,没有变化的数据是不会更新到数据库的,如果所有数据都没变化,那么根本就不会去执行数据库的更新操作。

所以你其实会发现后面的模型更新方法其实根本没执行(因为模型的更新数据和原有数据是一样的,没任何变化,当然你有对数据作了自动操作另当别论),但你更改name或者email属性的值的话,就会发现执行了更新操作。

更新操作用法小结:

方法 作用 返回值
save 更新数据 影响的记录数
update 更新数据(静态) 返回模型对象实例

删除Delete

Db类实现

Db::table('user')
    ->delete(1);

模型类实现

$user = User::get(1);
$user->delete();

或者静态实现

User::destroy(1);

delete方法没有任何参数,因此只能删除当前实例的模型数据,destroy方法支持删除指定主键或者查询条件的数据,例如:

// 根据主键删除多个数据
User::destroy([1, 2, 3]);
// 指定条件删除数据
User::destroy([
    'status' => 0,
]);
// 使用闭包条件
User::destroy(function ($query) {
    $query->where('id', '>', 0)
        ->where('status', 0);
});

早期版本的destroy方法如果传入空值,会删除数据表的所有数据,该问题已经在V5.0.9版本得到修正(不会执行任何删除)。

在模型的删除功能设计的时候,应该尽量用软删除替代实际的删除,一方面是为了避免数据丢失,一方面也是为了性能考虑(数据库的删除操作会导致重建索引,数据量越大影响越大),关于软删除的用法我们放到高级用法中描述。

删除操作用法小结:

方法 作用 返回值
delete 删除当前数据 影响的记录数
destroy 删除指定数据(静态) 影响的记录数

现在我们已经掌握了模型的基本CURD操作,我们来总结下方法区别:

用法 Db类 模型(动态) 模型(静态)
创建 insert save create
更新 update save update
读取单个 find find get
读取多个 select select all
删除 delete delete destroy

除了模型自己的方法操作外,还可以调用Db类的所有查询方法,也就是说Db类的CURD操作方法都可以在模型类中被调用。

不知道大家注意到一个细节没,模型的创建操作和更新操作的动态方法都是save,而并没区分。其实对于对象实例来说,所有的数据变化都只需要有一个保存行为,至于是创建还是更新那是数据库内部的事情,对不起模型对象不关心。模型会根据当前的场景自动判断是创建还是更新操作。

然后要注意几个注意事项:

  1. 模型类可以直接调用Db类的所有方法;
  2. 模型类和Db类的查询返回类型是完全不同的,即便是调用同一个方法查询;
  3. 模型类封装的静态方法本质上还是调用的动态方法,只是为了方便不同的需求场景;
  4. 模型对象的查询操作尽量使用静态方法调用;

使用查询构造器

之前我们已经知道了,Db类的所有方法都可以在模型中调用,因此查询构造器的用法在模型类中没有变化,并且还做了一些增强来支持模型的CURD封装方法。

下面举几个例子说明,首先是直接使用查询类提供的链式方法完成查询:

$users = User::where('name', 'like', '%think')
    ->where('id', 'between', [1, 5])
    ->order('id desc')
    ->limit(5)
    ->select();

所有的链式方法都可以直接被模型类静态调用,而且一样不分先后次序,你只要掌握了数据库的查询构造器用法,就能掌握模型的查询用法,而且模型类不需要调用table方法来指定数据表名称,因为模型已经有自己的对应数据表规则,从这一点来说,模型的查询操作应该比Db类的查询操作用法简单_

模型可以直接调用Db类(确切的说是查询类)的方法,无论是静态还是动态调用,也就是说你可以把模型类当成Db类一样使用(虽然用法一样,但其实区别很大,可能查询条件、查询结果和返回类型都不同),这得益于模型类和查询器类的友好邦交,在某些特殊情况下(例如不希望执行全局查询范围)可以这样调用查询器类的方法。

$user = (new User)
    ->db(false)
    ->where('name', 'thinkphp')
    ->find();

db方法是获取当前模型的数据库查询对象的方法,正常使用情况下我们不需要显式调用db方法,该方法当传入false的时候表示不使用全局查询范围。

模型类提供的all方法除了上面提过的根据主键值查询之外,还支持使用闭包查询,闭包方法中可以使用任何的查询类方法(但不需要在闭包里面调用查询),针对上面的查询我们可以用闭包方式改造如下:

$users = User::all(function ($query) {
    $query->where('name', 'like', '%think')
        ->where('id', 'between', [1, 5])
        ->order('id desc')
        ->limit(5);
});

闭包只有一个参数,就是查询对象。

如果你的查询参数都是以查询条件为主的话,可以给all方法直接传入数组查询条件即可:

$users = User::all([
    'name' => 'thinkphp',
    'id'   => ['>', 1],
]);

all方法如果传入索引数组,即表示查询条件,如果是不带索引的数组,表示查询多个主键。

模型的CURD方法其实用法不仅如此,模型的getall方法还有很多的用法和参数,更多的功能我们会在后面慢慢叙说。不过相信到目前为止,你已经对模型的CURD操作基本了解了。

使用Db类操作数据库的话,同一个连接器类调用的是同一个查询器类实例,而使用模型进行查询操作的话,每个模型对应的是独立的查询器类实例。每个查询器类实例都对应一个生成器类实例。

数据集

模型的单个数据查询返回的都是模型对象实例,但查询多个数据的时候默认返回的是一个包含模型对象实例的数组。框架提供了一个Collection数据集对象来进行统一的模型的对象化操作,替代默认的数组数据集更好的封装自己的数据处理和业务逻辑。

设置数据集对象后,查询多个数据的方法(包括Db类的select和模型类的all方法)返回的结果类型就会变成think\model\Collection对象实例。

有两种方式可以设置,第一种方式是全局设置数据库的配置参数(默认设置为array):

// 设置数据集返回类型
'resultset_type'  => 'collection',

该设置会影响所有的查询(包括Db类和模型类)。

第二种方式是在模型类中添加属性设置

// 设置模型的数据集返回类型
protected $resultSetType = 'collection';

该设置仅仅影响设置的模型中的查询结果,如果需要多个模型或者全部模型支持,可以使用继承或者使用第一种数据库配置方式。

数据集对象和普通的二维数组在使用上的一个最大的区别就是数据是否为空的判断,二维数组的数据集判断数据为空直接使用

$resultSet = User::all();
if (empty($resultSet)) {
    echo '数据集为空';
}

如果使用数据集对象的话,需要改成:

$resultSet = User::all();
if ($resultSet->isEmpty()) {
    echo '数据集为空';
}

通用的判断数据是否为空的方式可以用

$resultSet = User::all();
if (0 == count($resultSet)) {
    echo '数据集为空';
}

其它操作的区别就是一个是对象的方法操作,一个是数组函数的操作,下面是数据集对象的方法和数组函数的对应关系:

作用 数据集方法 数组函数
合并数据 merge array_merge
比较数据差集 diff array_diff
交换数组中的键和值 flip array_flip
比较数组交集 intersect array_intersect
返回键名 keys array_keys
最后元素出栈 pop array_pop
数组迭代简化 reduce array_reduce
数据反序 reverse array_reverse
首个元素出栈 shift array_shift
开头插入元素 unshift array_unshift
元素回调 each ---
过滤元素 filter array_filter
返回指定列 column array_column
元素排序 sort array_sort
打乱元素 shuffle shuffle
截取部分元素 slice array_slice
元素分割 chunk array_chunk
转换数组 toArray ---

可以自定义数据集的返回对象,然后在里面封装其它的方法。

一般自定义的数据集对象建议继承think\model\Collection,然后在模型中设置resultSetType属性值为自定义查询类的类名。

// 设置模型的数据集返回类型
protected $resultSetType = 'app\common\Collection';

总结下数据集的优势:

  • 数据更对象化;
  • 关联操作更方便;
  • 数据集本身可以单独定义独立的业务方法;

分页查询

分页查询其实也是查询多个数据的一种特殊方式,表现出来通常是页面上有很多的页数、当前页数和上下翻页按钮,而分页数据通常是配合数据库的limit语法来实现分页查询,普通的分页查询通常需要两个步骤:首先查询满足条件的记录总数,然后查询当前分页的数据。然而内置分页查询只需要调用paginate方法就可以实现分页查询,下面是查询代码。

<?php

namespace app\index\controller;

use app\index\model\User;
use think\Controller;

class Index extends Controller
{
    public function index($p=1)
    {
        // 查询分页数据
        $list = User::where('status', 1)->paginate();
        // 创建分页显示
        $this->assign('page', $list);
        // 模板渲染输出
        return $this->fetch();
    }
}

模板文件中分页输出代码如下:

<div>
总记录数:{$page->total()}
<ul>
{volist name='page' id='user'}
    <li> {$user.name}</li>
{/volist}
</ul>
</div>
{$page->render()}

paginate方法之后不需要调用任何的查询方法,该方法本身就是一个查询数据集的方法,而且返回结果是一个think\Paginator对象,该对象具有数据集对象的类似特性。

使用paginate方法查询不需要单独查询记录总数,也不需要使用limit或者page方法,通常作为全局分页配置可以在配置文件中设置下面的分页参数:

    //分页配置
    'paginate'               => [
        // 分页类
        'type'      => 'bootstrap',
        // 分页变量
        'var_page'  => 'p',
        // 每页记录数
        'list_rows' => 15,
    ],

除了这几个参数外,还可以在paginate方法调用的时候动态传入

$list = User::where('status', 1)->paginate([
    'type'      => 'bootstrap',
    'var_page'  => 'p',
    'list_rows' => 15,
]);

额外的分页参数包括:

参数 描述
page 指定当前页
path 当前url路径
query url额外参数(数组)
fragment url锚点

当你的分页查询条件来自于URL,需要传入query参数。

大部分情况,可能只需要传入每页的记录数,直接传入数字就表示设置分页的每页记录数。

$list = User::where('status', 1)
    ->paginate(20);

对于一些复杂的查询条件,尤其是使用了joingroup之类的,可能需要单独查询记录总数:

$total = User::where('status', 1)->count();
$list  = User::where('status', 1)
    ->paginate(20, $total);

对于某些应用,可能并不需要完整的分页显示,而只需要显示上一页和下一页,这种我们称之为简洁模式分页,对于这种情况,我们只需要在第二个参数传入true即可,简洁模式的分页优势是不需要查询记录总数。

// 简洁模式分页
$list = User::where('status', 1)
    ->paginate(20, true);

添加业务逻辑

模型的优势不是用来做基础的CURD操作的,虽然CURD操作也是一种最常见的业务逻辑,只是这些基本逻辑无需再定义额外的方法了,系统已经内置实现了。但实际的应用中,一般都需要根据业务需求来增加额外的业务逻辑方法。

User模型为例,假设我们需要实现下列功能:

  • 用户注册;
  • 用户登陆;
  • 获取用户信息;
  • 获取用户的身份角色;
  • ...更多业务逻辑

那么可以在User模型添加下面的逻辑方法:

<?php
namespace app\index\model;

use think\Model;

class User extends Model
{

    /**
     * 注册一个新用户
     * @param  array $data 用户注册信息
     * @return integer|bool  注册成功返回主键,注册失败-返回false
     */
    public function register($data = [])
    {
        $result = $this->validate(true)->allowField(true)->save($data);
        if ($result) {
            return $this->getData('id');
        } else {
            return false;
        }
    }

    /**
     * 用户登录认证
     * @param  string  $username 用户名
     * @param  string  $password 用户密码
     * @return integer 登录成功-用户ID,登录失败-返回0或-1
     */
    public function login($username, $password)
    {
        $where['username'] = $username;
        $where['status']   = 1;
        /* 获取用户数据 */
        $user = $this->where($where)->find();
        if ($user) {
            if (md5($password) != $user->password) {
                $this->error = '密码错误';
                return 0;
            } else {
                return $user->id;
            }
        } else {
            $this->error = '用户不存在或被禁用';
            return -1;
        }
    }

    /**
     * 获取用户信息
     * @param  integer  $uid 用户主键
     * @return array|integer 成功返回数组,失败-返回-1
     */
    public function info($uid)
    {
        $user = $this->where('id', $uid)->field('id,username,email,mobile,status')->find();
        if ($user && 1 == $user->status) {
            // 返回用户数据
            return $user->hidden('status')->toArray();
        } else {
            $this->error = '用户不存在或被禁用';
            return -1;
        }
    }

    /**
     * 获取用户角色
     * @return integer 返回角色信息或者返回-1
     */
    public function role()
    {
        $uid = $this->getData('id');
        if ($uid) {
            $role = $this->getUserRole($uid);
            if ($role) {
                return $role;
            } else {
                $this->error = '用户未授权';
                return 0;
            }
        } else {
            $this->error = '请先登录';
            return -1;
        }
    }

    protected function getUserRole($uid)
    {
        return $this->table('role')->where('uid', $uid)->find();
    }
}

我们先不要在意方法的实现细节(这些实现代码并非完美,只是简单的说明问题),里面的很多调用方法后面都会一一提及,这里只是告诉你如何在模型类里面添加自己的业务逻辑,下面同时给出在控制器中的调用参考。

<?php
namespace app\index\controller;

use app\index\model\User;
use think\Controller;
use think\Session;

class Index extends Controller
{
    public function login()
    {
        return $this->fetch();
    }

    public function doLogin(User $user, $username, $password)
    {
        $uid = $user->login($username, $password);
        if ($uid) {
            Session::set('user_id', $uid);
            $this->success('登录成功');
        } else {
            $this->error('登录失败');
        }
    }

    public function register()
    {
        return $this->fetch();
    }

    public function doRegister(User $user)
    {
        $data   = $this->request->param();
        $result = $user->register($data);
        if ($result) {
            $this->success('用户注册成功');
        } else {
            $this->error($user->getError());
        }
    }

    public function getUserInfo(User $user, $uid)
    {
        $info = $user->info($uid);
        if ($info) {
            $this->assign('user', $info);
            return $this->fetch();
        } else {
            return '用户不存在';
        }
    }

    protected function getUserRole()
    {
        $uid  = Session::get('user_id');
        $user = User::get($uid);
        return $user->role();
    }
}

控制器的详细用法不属于本书的讨论范畴,如果有必要可以参考官方快速入门系列第三部:《控制器从入门到精通》。

从上面的用法中我们可以注意几点:

  • 业务逻辑应当封装到具体模型中,并由控制器来调用;
  • registerlogin方法获取用户主键的方法区别;
  • 可以设置模型的错误信息,并且用getError方法获取;

总结

通过本章的学习,你应该掌握了模型的基本概念和基础用法,下一章我们就来学习模型的各种数据处理功能,下一章的内容更精彩哦。

上一篇:第四章:高级查询技巧
下一篇:第六章:模型数据处理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容