Laravel 文件閱讀:Eloquent 起步(下篇)

zhangbao發表於2017-08-30

翻譯、衍生自: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()
    {
        //
    }
}

相關文章