Thinkphp 第七章:模型高级用法

本章带你了解下模型的一些高级技巧,这些技巧会让你在使用模型的过程中更加高效和简单,学习内容主要包含:

条件查询

我们前面的大多数例子都是使用主键进行模型数据查询,事实上,业务逻辑并非都是如此简单,条件查询的必要性是存在的。

最简单的方法是直接使用Db类的查询构造器查询,方法是在模型中静态调用Db类(其实是查询类)的任何方法(包括动态查询)进行查询。

// 查询单个记录
User::where('name', 'thinkphp')->find();
// 调用动态查询方法
User::getByName('thinkphp');
// 查询数据集
User::where('id', '>', 0)->limit(10)->order('id desc')->select();
// 删除数据
User::where('status', 0)->delete();

如果你的查询代码在模型内部,可以直接支持动态方式调用查询构造器的方法,用法就是从静态调用改为动态调用。

// 查询单个记录
$this->where('name', 'thinkphp')->find();
// 调用动态查询方法
$this->getByName('thinkphp');
// 查询数据集
$this->where('id', '>', 0)->limit(10)->order('id desc')->select();
// 删除数据
$this->where('status', 0)->delete();

不过类似的查询场景应当极力避免(个别场景例如查询范围等可能会涉及到此类用法),查询操作应当是静态调用,更新和删除操作则是动态方法调用。如果是在模型方法中查询其它模型的数据,第一种静态调用方式仍然适用。尤其不建议使用table方法在同一个模型实例中切换数据表查询,在模型中动态设置table属性的方式更加不可取(经常发现这种奇葩的用法),模型和数据表以及相应的业务逻辑是应当在创建的时候就相对固定的,应当极力避免在一个模型对象实例中查询操作多次不同数据。

模型查询的原则应当是每个模型对象实例操作一个唯一记录,对于数据集来说这个原则也不变,只是每个数据集对象实例则包含多个模型对象实例而已。

对于自定义查询,如果统一使用模型类提供的getall进行查询也一样可以达到目的,改成下面代码即可:

// 查询单个记录
User::get(['name' => 'thinkphp']);
// 查询数据集
User::all(function ($query) {
    $query->where('id', '>', 0)
        ->limit(10)
        ->order('id desc');
});
// 删除数据
User::destroy(['status' => 0]);
User::destroy(function ($query) {
    $query->where('id', 'in', [1, 2, 3]);
});

getall以及destroy方法的参数用法记住一个原则,如果是数字、字符串或者普通数组都表示一个或者多个主键,如果是索引数组则表示查询条件,闭包则支持查询条件以外的其它链式操作方法。对于get方法的参数最好做一次非null检查,否则查询的就会是第一个数据(V5.0.8+已经改进,不需要检查是否为null了)。

查询范围

对于一些常用的查询条件,我们可以事先定义好,以便快速调用,这个事先定义的查询条件方法有一个统一的前缀scope,我们称之为查询范围,例如下面给User模型定义了两个查询范围方法。

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{

    // email查询
    protected function scopeEmail($query)
    {
        $query->where('email', 'thinkphp@qq.com');
    }

    // status查询
    protected function scopeStatus($query)
    {
        $query->where('status', 1);
    }
}

现在我们直接使用

$users = User::scope('email,status')->select();

或者使用

$users = User::scope('email')->scope('status')->select();

生成的查询语句是

SELECT * FROM `user` WHERE `email` = 'thinkphp@qq.com' AND `status` = 1 

查询范围之外仍然可以使用额外的查询条件,例如:

$users = User::scope('email,status')
    ->where('nickname', 'like', '%think%')
    ->order('id desc')
    ->select();

查询范围方法必须首先被调用

查询范围方法支持额外的参数,例如scopeEmail方法改为:

    // email查询
    protected function scopeEmail($query, $email = '')
    {
        $query->where('email', $email);
    }

查询范围的方法的第一个参数必须是查询对象,并且支持多个额外参数。

然后,使用下面的方式调用即可(带参数调用的时候每次只能调用一个查询范围):

$list = User::scope('email', 'thinkphp@qq.com')->select();

查询范围有一个特殊的方法base,一旦在模型中定义了base方法后,无需显式调用scope方法,系统会在每次查询的时候自动调用,我们称之为全局查询范围。

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    // 全局查询范围
    protected static function base($query)
    {
        // 查询状态为1的数据
        $query->where('status', 1);
    }

    // email查询
    protected function scopeEmail($query)
    {
        $query->where('email', 'thinkphp@qq.com');
    }

}

当使用下面的查询操作

User::get(1);
User::scope('email')->select();

最后生成的SQL语句分别是:

SELECT * FROM `user` WHERE `status` = 1 AND `id` = 1 LIMIT 1    
SELECT * FROM `user` WHERE `status` = 1 AND `email` = 'thinkphp@qq.com' 

无论是什么查询都会默认带上全局查询范围中的条件。

可以临时关闭全局查询范围进行查询

// 关闭全局查询范围
User::useGlobalScope(false)->get(1);

查询范围方法中不仅支持where方法,任何查询构造器的方法都可以被支持。

字段过滤

经常我们会直接使用表单提交的数据来作为模型数据写入,但并不是所有的数据都是数据表字段(直接写入会导致数据库异常),或者不希望某些数据被用户通过表单提交的方式更新(为了安全或者逻辑考虑),Request类自身提供了only方法来获取部分想要的数据,例如:

// 只获取请求变量中的nickname和address变量
$data = request()->only(['nickname', 'address']);
// 获取当前用户对象
$user = User::get(request()->session('user_id'));
// 更新用户数据
$user->data($data, true)->save();

模型类提供了allowField方法用于在数据写入操作的时候设置字段过滤,从而避免数据库因为字段不存在而报错,上面的写法可以简化为。

// 获取当前用户对象
$user = User::get(request()->session('user_id'));
// 只允许更新用户的nickname和address数据
$user->allowField(['nickname', 'address'])
    ->data(requst()->param(), true)
    ->save();

如果仅仅是希望去除数据表之外的字段,可以使用

// 只允许更新数据表字段数据
$user->allowField(true)
    ->data(requst()->param(), true)
    ->save();

为了不必每次都调用allowField方法,我们可以直接在模型类里面设置field属性,例如:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $field = ['name', 'nickname', 'email', 'address'];
}

当调用allowField方法的时候,当前模型实例中的该配置的值会被覆盖。

如果使用的是模型的静态方法(如createupdate方法)进行数据写入的话,可以使用下面的方式进行字段过滤。

User::create(request()->param(), ['nickname', 'address']);
User::update(request()->param(), ['id' => 1], ['nickname', 'address']);

同样可以传入true表示过滤非数据表字段

User::create(request()->param(), true);
User::update(request()->param(), ['id' => 1], true);

只读字段

有些数据字段在写入以后就不允许被更改,例如name字段和email字段,那么我们可以设置该字段为只读字段,在更新的时候就会被自动忽略掉。

设置方式:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $readonly = ['name','email'];
}

举个例子说明下:

$user = User::get(5);
echo $user->name;
echo $user->email;
// 更改某些字段的值
$user->name    = 'TOPThink';
$user->email   = 'Topthink@gmail.com';
$user->address = '上海静安区';
// 保存更改后的用户数据
$user->save();
echo $user->name;
echo $user->email;

事实上,由于我们对nameemail字段设置了只读,因此只有address字段的值被更新了,而nameemail的值仍然还是更新之前的值。

软删除

在实际项目中,对数据频繁使用删除操作会导致性能问题,因此不推荐直接物理删除数据,而是用逻辑删除替代,也就是下面要讲的软删除。软删除的作用就是把数据加上删除标记,而不是真正的删除,同时也便于需要的时候进行数据的恢复。

要使用软删除功能,需要引入SoftDelete trait,例如User模型按照下面的定义就可以使用软删除功能:

<?php

namespace app\index\model;

use think\Model;
use traits\model\SoftDelete;

class User extends Model
{
    use SoftDelete;
}

为了配合软删除功能,你需要在数据表中添加delete_time字段,ThinkPHP5的软删除功能使用时间戳类型(数据表默认值为Null),用于记录数据的删除时间。

如果你的软删除标记字段名称不是delete_time的话,需要添加属性定义:

<?php

namespace app\index\model;

use think\Model;
use traits\model\SoftDelete;

class User extends Model
{
    use SoftDelete;
    protected $deleteTime = 'delete_field_name';
}

可以用类型转换指定软删除字段的类型,建议数据表的所有时间字段统一使用autoWriteTimestamp属性规范时间类型(支持datetimedatetimestamp以及integer)。

<?php

namespace app\index\model;

use think\Model;
use traits\model\SoftDelete;

class User extends Model
{
    use SoftDelete;
    protected $autoWriteTimestamp = 'datetime';
    protected $deleteTime = 'delete_field_name';
}

定义好模型后,我们就可以使用:

// 软删除
User::destroy(1);
// 真实删除
User::destroy(1,true);
$user = User::get(1);
// 软删除
$user->delete();
// 真实删除
$user->delete(true);

默认情况下查询的数据不包含软删除数据,如果需要包含软删除的数据,可以使用下面的方式查询:

User::withTrashed()->find();
User::withTrashed()->select();

如果仅仅需要查询软删除的数据,可以使用:

User::onlyTrashed()->find();
User::onlyTrashed()->select();

如果你的查询条件比较复杂,尤其是某些特殊情况下使用OR查询条件会把软删除数据也查询出来,可以使用闭包查询的方式解决,如下:

User::where(function($query) {
    $query->where('id', '>', 10)
        ->whereOr('name', 'like', 'think');
})->select();

使用闭包查询条件会在查询条件两边添加括号,从而不会和软删除条件产生混淆或者冲突。

如果你的模型定义了base基础查询,请确保添加软删除的基础查询条件,例如:

protected function base($query)
{
    // 添加软删除条件
    $query->whereNull('delete_time')
        // 添加额外的基础查询条件
        ->where('id','>',0);
}

自定义查询类

默认情况下,默认使用的查询类是核心内置的think\db\Query类,如果你需要自己扩展额外的查询方法,可以自定义查询类,例如:

<?php

namespace app\db;

use think\db\Query;

class ModelQuery extends  Query
{

    public function top($num)
    {
        return $this->limit($num)-select();
    }
}

在模型类中设置query属性如下

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    protected $query = '\app\db\ModelQuery';
}

设置后,User模型就可以使用top方法查询

User::where('id desc')->top(10);

如果全局都使用该查询类的话,建议直接在数据库配置文件中使用下面配置:

// 设置Query类
'query'           => '\\app\\db\\ModelQuery',

总结

本章我们学习了模型的一些高级用法,下一章我们就来学习下模型关联的使用。

上一篇:第六章:模型数据处理
下一篇:第八章:模型关联

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,065评论 1 32
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 4,968评论 0 9
  • 原文:https://my.oschina.net/liuyuantao/blog/751438 查询集API 参...
    阳光小镇少爷阅读 3,802评论 0 8
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,588评论 0 15
  • 已经记不得是第几次搬家了。 看着那些刚刚安置在屋里为数不多的几个物件,眼眶有些湿润。 真想给他们道个歉,什么时候才...
    陈羲1874阅读 262评论 0 2