Laravel 文档阅读:Eloquent 起步(下篇)

翻译、衍生自:https://learnku.com/docs/laravel/5.4/eloquent

上篇

插入 & 更新 Model

插入

向数据库插入一条数据,可以采用这样的方式:创建一个模型实例、为实例设置属性,然后调用 save 方法:

<?php

namespace App\Http\Controllers;

use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class FlightController extends Controller
{
    /**
     * Create a new flight instance.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        // Validate the request...

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();
    }
}

在上面的例子里,我们把从 HTTP 请求里接受到的 name 参数赋值给了 App\Flight 模型实例对象的 name 属性。当 save 方法调用后,一条数据就被插入数据库了,而且 created_atupdated_at 字段也会被自动更新

更新

save 方法还可以用来更新 已存在于数据库中的数据。在更新模型数据前,先要获得模型实例对象,然后设置要更新的字段内容,就可以调用 save 方法更新数据了, updated_at 时间戳字段会被自动更新:

$flight = App\Flight::find(1);

$flight->name = 'New Flight Name';

$flight->save();

批量更新

update 方法可以用在批量更新数据库记录。在下面的例子里,所有以 San Diego 作为 destination 的、并且是 active 的航班,都要延迟起飞了:

App\Flight::where('active', 1)
          ->where('destination', 'San Diego')
          ->update(['delayed' => 1]);

update 方法里列出的字段就是要更新的字段。

当通过这种方式批量更新数据时,不会触发 savedupdated 事件。因为数据是直接在数据库更新的,没有通过取得、然后再更新的方式。

批量赋值

也可以用 create 方法保存模型实例对象,该方法会返回被插入的模型实例对象。但有前提的,前提是要在 Model 里设置 fillable 或者 guarded 属性。因为默认 Eloquent Model 是不允许批量赋值的。

批量赋值漏洞发生在用户通过 HTTP 请求传递了非预期的字段参数,并且那个字段参数对应到你的数据库里的那个字段,是你不想让用户更改的。例如,一个恶毒的用户可能通过 HTTP 请求传递了 is_admin 参数,而你呢,是使用 create 方法来创建用户的,这就相当于用户把自己改为超级管理员了,这很危险。

$fillable 属性

所以,你需要在 Model 中设置哪些字段是允许批量赋值的。可以通过设置 Model 上的 $fillable 属性实现。例如,在 Flight Model 上将 name 属性设置为可批量赋值的:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

设置好后,我们就可以用 create 方法向数据库插入数据了。 create 方法返回保存的 Model 实例对象:

$flight = App\Flight::create(['name' => 'Flight 10']);

如果已有了一个 Model 实例,通过 fill 方法的(数组类型的)参数可以填充 Model 数据:

$flight->fill(['name' => 'Flight 22']);

$guarded 属性

$fillable 属性相当于批量赋值的「白名单」,而 $guarded 属性呢就相当于一个「黑名单」。在一个 Model 里,不可以同时设定 $fillable$guarded 属性。在下面的例子中,除了 price 字段,其他都是可批量赋值的字段:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['price'];
}

如果要让所有的属性都是可以批量赋值的,那么将 $guarded 属性设置为空数组即可:

/**
 * The attributes that aren't mass assignable.
 *
 * @var array
 */
protected $guarded = [];

其他的创建方法

firstOrCreate / firstOrNew

这儿还有两个使用了批量赋值创建 Model 的方法:firstOrCreatefirstOrNewfirstOrCreate 方法用给到的字段键值去数据库查找是否有匹配的记录,没有匹配记录的话,就会将给出的字段键值数据插入到数据库表中。

firstOrNew 方法与 firstOrCreate 类似,也会去数据库查找是否有匹配的记录,不同的是,如果没有匹配记录的话,就会创建并返回一个新的 Model 实例。需要注意的是,通过 firstOrNew 方法返回的 Model 实例不会插入到数据库中,如果需要插入到数据库,就需要额外调用 save 方法:

// Retrieve flight by name, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);

// Retrieve flight by name, or create it with the name and delayed attributes...
$flight = App\Flight::firstOrCreate(
    ['name' => 'Flight 10'], ['delayed' => 1]
);

// Retrieve by name, or instantiate...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);

// Retrieve by name, or instantiate with the name and delayed attributes...
$flight = App\Flight::firstOrNew(
    ['name' => 'Flight 10'], ['delayed' => 1]
);

updateOrCreate

有时你也会遇到这样的情况:更新一个 Model 时,如果 Model 不存在就创建它。用 updateOrCreate 一步就可以做到。类似 firstOrCreate 方法,updateOrCreate 会持久化 Model,所以无需手动调用 save()

// If there's a flight from Oakland to San Diego, set the price to $99.
// If no matching model exists, create one.
$flight = App\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99]
);

删除 Model

在 Model 实例上调用 delete 方法删除它:

$flight = App\Flight::find(1);

$flight->delete();

通过主键删除

在已知主键的情况下,可以直接删除,而无需先获得。这时,使用 destory 方法:

App\Flight::destroy(1);

App\Flight::destroy([1, 2, 3]);

App\Flight::destroy(1, 2, 3);

删除一组 Model

当然,也可以在一组 Model 上运行 delete 语句。 在下面的例子中,我们会删除所有 inactive 的航班。类似批量更新,批量删除不会触发模型事件 deletingdeleted

$deletedRows = App\Flight::where('active', 0)->delete();

软删除

“软删除”并不是把数据记录从数据库中真的删除,而是在 deleted_at 时间戳字段上设置了值,当这个字段的值不是 null 时,我们认为这条记录就是被删除了。 为了让一个 Model 支持软删除,需要让 Model 引入 Illuminate\Database\Eloquent\SoftDeletes 这个 trait,并且将 deleted_at 字段加入到 $dates 属性中:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}

当然,在数据库表里也要添加 deleted_at 这个字段。Laravel 的 schema 构造器中提供了创建这个字段的方法:

Schema::table('flights', function ($table) {
    $table->softDeletes();
});

现在,当你调用 delete 方法删除 Model 时,delete_at 字段就会被设置为当前执行删除操作时的时间,当用查询获得数据时,被软删除的数据记录会被自动排除在外,不显示。

判断一个 Model 实例是否被删除,使用 trashed 方法:

if ($flight->trashed()) {
    //
}

查询软删除 Model

包括软删除数据

之前提到过,查询时,被软删除的数据记录会被自动排除在外。你也可以强制查询结果里包含软删除数据,需要在查询上用到 withTrashed 方法:

$flights = App\Flight::withTrashed()
                ->where('account_id', 1)
                ->get();

withTrashed 方法也可以用在关联查询上:

$flight->history()->withTrashed()->get();

只获取软删除数据

onlyTrashed 方法只返回软删除数据:

$flights = App\Flight::onlyTrashed()
                    ->where('airline_id', 1)
                    ->get();

还原软删除 Model

还原软删除 Model,需要使用 restore 方法:

$flight->restore();

也可以使用 restore 方法还原多条数据,不过就像其他的“批量”操作,不会触发任何模型事件:

App\Flight::withTrashed()
        ->where('airline_id', 1)
        ->restore();

类似 withTrashed 方法,restore 方法也可以用在关联方法上:

$flight->history()->restore();

彻底删除 Model

如果是真的要把记录从数据库删除,要用到 forceDelete 方法:

 // Force deleting a single model instance...
$flight->forceDelete();

// Force deleting all related models...
$flight->history()->forceDelete();

查询范围

全局查询范围

全局查询范围是给一个 Model 的所有查询添加约束条件。Laravel 自身的软删除功能就是利用了全局查询范围,只从数据库中获得“未删除”的 Model。自定义全局查询范围可以更方便、快捷地给指定 Model 的所有查询添加特定约束。

写全局查询范围

查询范围类需要实现 Illuminate\Database\Eloquent\Scope 接口,这个接口里需要实现一个方法:applyapply 方法可以给查询添加必要的 where 约束条件:

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AgeScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('age', '>', 200);
    }
}

注意,如果在全局查询范围类里的查询要使用 select 子句,你应该用 addSelect 方法而不是 select 方法。这将防止无意中替换现有查询中的 select 子句。

使用全局约束

想在指定 Model 上使用全局查询范围,可以通过重写 Model 的 boot 方法实现,在方法内部,再调用 addGlobalScope 方法:

<?php

namespace App;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new AgeScope);
    }
}

添加查询范围后,执行 User::all(),产生的 SQL 语句如下:

select * from `users` where `age` > 200

匿名全局查询范围

Eloquent 也允许使用闭包的方式定义全局查询范围,这对于不想用单独类的简单查询范围特别有用。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('age', function (Builder $builder) {
            $builder->where('age', '>', 200);
        });
    }
}

删除全局查询范围

如果对于一个给定的查询,不需要使用全局查询范围,那就要用 withoutGlobalScope 方法了,该方法接受全局查询范围类作为它的唯一参数:

User::withoutGlobalScope(AgeScope::class)->get();

如果要删除几个或者全部的全局查询范围,就需要用 withoutGlobalScopes 方法了:

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

本地查询范围

本地查询范围就是在 Model 中定义的,以方法形式呈现,方法名以 scope 开头。Scope 方法应该总是返回一个查询构造器实例:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}

使用本地查询范围

Scope 方法定义好后,在查询 Model 的时候就可以使用了。但是使用的时候,就不需要加上 scope 前缀了。也可以链式调用不同的 Scope 方法,例如:

$users = App\User::popular()->active()->orderBy('created_at')->get();

动态查询范围

有时,需要定义一个接受参数的 Scope 方法。这也很简单,在 $query 之后,添加我们希望接受的参数即可:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $type
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfType($query, $type)
    {
        return $query->where('type', $type);
    }
}

现在,你就可以在调用 Scope 方法的时候传递参数过去就行了:

$users = App\User::ofType('admin')->get();

事件

Eloquent Model 在整个生命周期内,提供了几个不同的事件,方便你在对应的关键点上添加处理逻辑:creatingcreatedupdatingupdatedsavingsaveddeletingdeletedrestoringrestored。这些事件允许你每次在数据库中保存或更新特定的 Model 类时轻松执行代码。

当一个新的 Model 第一次保存的时候,会触发 creatingcreated 事件。当同一个 Model 已存在于数据库中,并且调用了 save 方法,那么就会触发 updating / updated 事件。而且这两种情况,都会触发 saving / saved 事件,事件的调用顺序依次如下:

第一次保存:saving → creating → created → saved
使用 `save()` 更新已有数据:saving → updating → updated → saved

在你的 Eloquent Model 中定义一个 $events 属性为 Eloquent Model 在整个生命周期的事件点上指定事件处理逻辑的一个映射表,这里需要用到 事件类

<?php

namespace App;

use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $events = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

观察者

如果在一个 Model 中需要监听的事件很多,那么这时就可以用“观察者”将所有监听逻辑组织在一个类中。观察者类中方法名对应事件名。每个事件接受 Model 实例作为他们的唯一参数。Laravel 没有为观察者默认创建文件夹,所以你可能要为你的观察者创建存放它们的文件夹了。在下面的例子中,观察者放在了 app 目录下的 Observers 文件夹下:

<?php

namespace App\Observers;

use App\User;

class UserObserver
{
    /**
     * Listen to the User created event.
     *
     * @param  User  $user
     * @return void
     */
    public function created(User $user)
    {
        //
    }

    /**
     * Listen to the User deleting event.
     *
     * @param  User  $user
     * @return void
     */
    public function deleting(User $user)
    {
        //
    }
}

是在服务提供者的 boot 方法里注册观察者的,使用的是 Model 的 observer 方法。在下面的例子里,我们将观察者注册在了 AppServiceProvider 中:

<?php

namespace App\Providers;

use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class);
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 7年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!