Laravel 文档阅读: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_at
和 updated_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
方法里列出的字段就是要更新的字段。
当通过这种方式批量更新数据时,不会触发 saved
和 updated
事件。因为数据是直接在数据库更新的,没有通过取得、然后再更新的方式。
批量赋值
也可以用 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 的方法:firstOrCreate
和 firstOrNew
。firstOrCreate
方法用给到的字段键值去数据库查找是否有匹配的记录,没有匹配记录的话,就会将给出的字段键值数据插入到数据库表中。
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 的航班。类似批量更新,批量删除不会触发模型事件 deleting
和 deleted
。
$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
接口,这个接口里需要实现一个方法:apply
。apply
方法可以给查询添加必要的 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 在整个生命周期内,提供了几个不同的事件,方便你在对应的关键点上添加处理逻辑:creating
、created
、updating
、updated
、saving
、saved
、deleting
、deleted
、restoring
和 restored
。这些事件允许你每次在数据库中保存或更新特定的 Model 类时轻松执行代码。
当一个新的 Model 第一次保存的时候,会触发 creating
和 created
事件。当同一个 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 协议》,转载必须注明作者和本文链接