Laravel 5.5 Eloquent ORM - 快速入門
簡介
Laravel 內建的 Eloquent ORM 提供了一個美觀、簡單的與資料庫打交道的 ActiveRecord 實現。
每張資料表都對應一個與該表進行互動的模型(Model),通過模型類,你可以對資料表進行查詢、插入、更新、刪除等操作。
定義模型
我們從建立一個 Eloquent 模型開始,模型類預設位於 app 目錄下,但推薦將模型存放在 app/Models 目錄下。
所有 Eloquent 模型都繼承自 Illuminate\Database\Eloquent\Model 類。
建立模型最簡單的方法就是使用 Artisan 命令 make:model。
php artisan make:model Test
上述命令會自動生成 app/Test.php 模型檔案。
為了將模型存放到 app/Models 目錄,在使用 Artisan 命令 make:model 時,可以在模型名稱的前面加上一個相對目錄名 Models/。
php artisan make:model Models/Test
這樣,就會自動生成 app/Models/Test.php 模型檔案。內容如下:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Test extends Model
{
//
}
如果你想要在生成模型時生成資料庫遷移,可以使用 --migration 或 -m 選項:
php artisan make:model User --migration
php artisan make:model User -m
Eloquent 模型約定
現在,讓我們來看一個 Flight 模型的例子,我們將用該類獲取和存取資料表 flights 中的資訊:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
//
}
表名
在本例中,我們並沒有明確指定 Flight 模型使用哪張表。
預設規則是小寫的模型類名的複數格式作為與其對應的表名(除非在模型類中明確指定了資料表的名稱)。
所以,在本例中,Eloquent 認為 Flight 模型對應的是 flights 表。
你也可以在模型中定義 table 屬性來指定自定義的表名。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 與模型對應的資料表
*
* @var string
*/
protected $table = 'my_flights';
}
主鍵
Eloquent 預設每張表的主鍵名為 id,你可以在模型類中定義一個 $primaryKey 屬性來覆蓋該約定。
此外,Eloquent 預設主鍵欄位是自增的整型資料,這意味著主鍵將會被自動轉化為 int 型別,如果你想要使用非自增或非數字型別主鍵,必須在對應模型中設定 $incrementing 屬性為 false,如果主鍵不是整型,還要設定 $keyType 屬性值為 string。
時間戳
預設情況下,Eloquent 期望 created_at 和 updated_at 已經存在於資料表中。
如果你不讓 Laravel 自動管理這些欄位,就在模型類中設定 $timestamps 屬性為 false。
public $timestamps = false;
如果你需要自定義時間戳格式,就設定模型中的 $dateFormat 屬性。該屬性決定日期被如何儲存到資料庫中,以及模型被序列化為陣列或 JSON 時日期的格式。
protected $dateFormat = 'U';
如果你需要自定義用於儲存時間戳的欄位名稱,可以在模型中設定 CREATED_AT 和 UPDATED_AT 常量:
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'last_update';
資料庫連線
預設情況下,所有的 Eloquent 模型使用應用配置中的預設資料庫連線,如果你想要為模型指定不同的連線,可以通過 $connection 屬性來設定。
protected $connection = 'connection-name';
獲取模型
建立完模型及其關聯的資料表後,就可以從資料庫中獲取資料了。
將 Eloquent 模型看作功能強大的查詢構建器,你可以使用它來流暢的查詢與其關聯的資料表。
<?php
use App\Flight;
$flights = App\Flight::all();
foreach ($flights as $flight) {
echo $flight->name;
}
Eloquent 的 all 方法返回模型表的所有結果,由於每一個 Eloquent 模型都是一個查詢構建器,你還可以新增約束條件到查詢,然後使用 get 方法獲取對應結果。
$flights = App\Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
注:由於 Eloquent 模型本質上就是查詢構建器,你可以在 Eloquent 查詢中使用查詢構建器的所有方法。
集合
對於 Eloquent 中獲取多個結果的方法(比如 all 和 get)而言,其返回值是 Illuminate\Database\Eloquent\Collection 的一個例項,Collection 類提供了多個有用的函式來處理 Eloquent 結果集。
$flights = $flights->reject(function ($flight) {
return $flight->cancelled;
});
當然,你也可以像陣列一樣迴圈遍歷該集合:
foreach ($flights as $flight) {
echo $flight->name;
}
組塊結果集
如果你需要處理資料量很大的 Eloquent 結果集,可以使用 chunk 方法。chunk 方法會獲取一個指定數量的 Eloquent 模型“組塊”,並將其填充到給定閉包進行處理。
使用 chunk 方法在處理大量資料集合時能夠有效減少記憶體消耗。
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});
傳遞給該方法的第一個引數是你想要獲取的“組塊”數目,閉包作為第二個引數被傳入用於處理每個從資料庫獲取的組塊資料。
使用遊標
cursor 方法允許你使用遊標迭代處理資料庫記錄,一次只執行單個查詢,在處理大批量資料時,cursor 方法可大幅減少記憶體消耗。
foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
//
}
獲取單個模型/聚合結果
除了從給定表中獲取所有記錄之外,還可以使用 find 和 first 獲取單個記錄。
這些方法返回單個模型例項而不是模型集合。
// 通過主鍵獲取模型...
$flight = App\Flight::find(1);
// 獲取匹配查詢條件的第一個模型...
$flight = App\Flight::where('active', 1)->first();
還可以通過傳遞主鍵陣列來呼叫 find 方法,這將會返回匹配記錄集合:
$flights = App\Flight::find([1, 2, 3]);
Not Found 異常
有時候你可能想要在模型找不到的時候丟擲異常,這在路由或控制器中非常有用,findOrFail 和 firstOrFail 方法會獲取查詢到的第一個結果。
如果沒有任何查詢結果,Illuminate\Database\Eloquent\ModelNotFoundException 異常將會被丟擲。
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();
如果異常沒有被捕獲,那麼 HTTP 404 響應將會被髮送給使用者,所以在使用這些方法的時候沒有必要對返回 404 響應編寫額外的檢查。
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});
獲取聚合結果
你還可以使用查詢構建器提供的聚合方法,如 count、sum、max 等。
這些方法返回的是計算後的結果,而不是整個模型例項。
$count = App\Flight::where('active', 1)->count();
$max = App\Flight::where('active', 1)->max('price');
插入/更新模型
插入
想要在資料庫中插入新的記錄,只需例項化對應的模型例項,設定模型例項的屬性,然後呼叫 save 方法。
<?php
namespace App\Http\Controllers;
use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class FlightController extends Controller{
/**
* 新增記錄
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
// 驗證請求...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
}
在這個例子中,我們只是簡單分配 HTTP 請求中的 name 引數值給 App\Flight 模型例項的 name 屬性,當我們呼叫 save 方法時,一條記錄將會被插入資料庫。created_at 和 updated_at 時間戳在 save 方法被呼叫時會自動被設定,所以沒必要手動設定它們。
更新
save 方法還可以用於更新資料庫中已存在的模型。要更新一個模型,應該先獲取它,設定你想要更新的屬性,然後呼叫 save 方法。同樣,updated_at 時間戳會被自動更新,所以沒必要手動設定其值。
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
批量更新
更新操作還可以同時修改給定查詢提供的多個模型例項,在本例中,所有有效且 destination=San Diego 的航班都被標記為延遲。
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update 方法要求以陣列形式傳遞鍵值對引數,代表著資料表中應該被更新的列。
注:通過 Eloquent 進行批量更新時,saved 和 updated 模型事件將不會在更新模型時觸發。這是因為在進行批量更新時並沒有從資料庫獲取模型。
批量賦值
還可以使用 create 方法儲存一個新的模型。該方法返回被插入的模型例項。
但是,在此之前,你需要指定模型的 fillable 或 guarded 屬性,因為所有 Eloquent 模型都通過批量賦值(Mass Assignment)進行保護,這兩個屬性分別用於定義哪些模型欄位允許批量賦值以及哪些模型欄位是受保護的,不能顯式進行批量賦值。
當用戶通過 HTTP 請求傳遞一個不被期望的引數值時就會出現安全隱患,然後該引數以不被期望的方式修改資料庫中的欄位值。例如,惡意使用者通過 HTTP 請求傳送一個 is_admin 引數,然後該引數對映到模型的 create 方法,從而允許使用者將自己變成管理員。
所以,你應該在模型中定義哪些屬性是可以進行賦值的,使用模型上的 $fillable 屬性即可實現。
例如,我們設定 Flight 模型上的 name 屬性可以被賦值:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 可以被批量賦值的屬性.
*
* @var array
*/
protected $fillable = ['name'];
}
設定完可以被賦值的屬性之後,我們就可以使用 create 方法在資料庫中插入一條新的記錄。create 方法返回儲存後的模型例項:
$flight = App\Flight::create(['name' => 'Flight 10']);
如果你已經有了一個模型例項,可以使用 fill 方法通過陣列屬性來填充:
$flight->fill(['name' => 'Flight 22']);
黑名單屬性
$fillable 就像是可以被賦值屬性的“白名單”,還可以選擇使用 $guarded。
$guarded 屬性包含你不想被賦值的屬性陣列。所以不被包含在其中的屬性都是可以被賦值的,因此,$guarded 功能就像“黑名單”。
當然,這兩個屬性你只能同時使用其中一個而不能一起使用,因為它們是互斥的。
下面的例子中,除了 price 之外的所有屬性都是可以賦值的:
/**
* 不能被批量賦值的屬性
*
* @var array
*/
protected $guarded = ['price'];
如果你想要讓所有屬性都是可批量賦值的,可以將 $guarded 屬性設定為空陣列:
protected $guarded = [];
其它建立方法
firstOrCreate/firstOrNew
firstOrCreate 方法先嚐試通過給定列/值對在資料庫中查詢記錄,如果沒有找到的話則通過給定屬性建立一個新的記錄。
firstOrNew 方法和 firstOrCreate 方法一樣先嚐試在資料庫中查詢匹配的記錄,如果沒有找到,則返回一個新的模型例項。
需要注意的是,通過 firstOrNew 方法返回的模型例項並沒有持久化到資料庫中,你還需要呼叫 save 方法手動持久化。
// 通過屬性獲取航班, 如果不存在則建立...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// 通過name獲取航班,如果不存在則通過name和delayed屬性建立...
$flight = App\Flight::firstOrCreate(
['name' => 'Flight 10'], ['delayed' => 1]
);
// 通過屬性獲取航班, 如果不存在初始化一個新的例項...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
// 通過name獲取,如果不存在則通過name和delayed屬性建立新例項...
$flight = App\Flight::firstOrNew(
['name' => 'Flight 10'], ['delayed' => 1]
);
updateOrCreate
你還會碰到如果模型已存在則更新,否則建立新模型的場景,Laravel 提供了一個 updateOrCreate 方法來一步完成。
和 firstOrCreate 方法一樣,updateOrCreate 方法會持久化模型,所以無需呼叫 save()。
// 如果有從奧克蘭到聖地亞哥的航班則將價格設定為 $99
// 如果沒有匹配的模型則建立之
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99]
);
刪除模型
要刪除一個模型,可呼叫模型例項上的 delete 方法。
$flight = App\Flight::find(1);
$flight->delete();
通過主鍵刪除模型
在上面的例子中,我們在呼叫 delete 方法之前從資料庫中獲取了該模型,不過,如果你知道模型的主鍵的話,可以呼叫 destroy 方法直接進行刪除而不需要事先獲取。
App\Flight::destroy(1);
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(1, 2, 3);
通過查詢刪除模型
當然,你還可以通過查詢刪除多個模型,在本例中,我們刪除所有被標記為無效的航班。
$deletedRows = App\Flight::where('active', 0)->delete();
注:通過 Eloquent 進行批量刪除時,deleting 和 deleted 模型事件在刪除模型時不會被觸發,這是因為在進行模型刪除時不會獲取模型。
軟刪除
除了從資料庫物理刪除記錄外,Eloquent 還可以對模型進行“軟刪除”。
當模型被軟刪除後,它們並沒有真的從資料庫刪除,而是在模型上設定一個 deleted_at 屬性並插入資料庫,如果模型有一個非空 deleted_at 值,那麼該模型已經被軟刪除了。
要啟用模型的軟刪除功能,可以使用模型上的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;
/**
* 應該被調整為日期的屬性
*
* @var array
*/
protected $dates = ['deleted_at'];
}
當然,應該新增 deleted_at 欄位到資料表。Laravel Schema 構建器包含一個輔助函式來建立該資料列:
Schema::table('flights', function ($table) {
$table->softDeletes();
});
現在,當呼叫模型的 delete 方法時,deleted_at 列將被設定為當前日期和時間,並且,當查詢一個使用軟刪除的模型時,被軟刪除的模型將會自動從查詢結果中排除。
判斷給定模型例項是否被軟刪除,可以使用 trashed 方法:
if ($flight->trashed()) {
//
}
查詢被軟刪除的模型
包含軟刪除模型
正如上面提到的,軟刪除模型將會自動從查詢結果中排除,不過,如果你想要軟刪除模型出現在查詢結果中,可以使用 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();
恢復軟刪除模型
有時候你希望恢復某個被軟刪除的模型,可以使用 restore 方法:
$flight->restore();
你還可以在查詢中使用 restore 方法來快速恢復多個模型,同樣,這也不會觸發任何模型事件:
App\Flight::withTrashed()
->where('airline_id', 1)
->restore();
和 withTrashed 方法一樣,restore 方法也可以用於關聯查詢:
$flight->history()->restore();
永久刪除模型
有時候你真的需要從資料庫中刪除一個模型,要從資料庫中永久刪除記錄,可以使用 forceDelete 方法:
// 強制刪除單個模型例項...
$flight->forceDelete();
// 強制刪除所有關聯模型...
$flight->history()->forceDelete();
查詢作用域
全域性作用域
全域性作用域允許我們為給定模型的所有查詢新增條件約束。
Laravel 自帶的軟刪除功能就使用了全域性作用域來從資料庫中拉出所有沒有被刪除的模型。
編寫自定義的全域性作用域可以提供一種方便的、簡單的方式來確保給定模型的每個查詢都有特定的條件約束。
編寫全域性作用域
自定義全域性作用域很簡單,首先定義一個實現 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
{
/**
* 應用作用域到給定的Eloquent查詢構建器.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
return $builder->where('age', '>', 200);
}
}
Laravel 預設並沒有為作用域類檔案,預定義資料夾,所以你可以按照自己的喜好在 app 目錄下建立 Scopes 目錄,以存放作用域類。
注:如果你的全域性作用域需要新增列到查詢的 select 子句,需要使用 addSelect 方法來替代 select,這樣就可以避免對已存在的 select 查詢子句造成影響。
應用全域性作用域
要將全域性作用域應用到模型,需要重寫給定模型的 boot 方法並使用 addGlobalScope 方法。
<?php
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的“啟動”方法.
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope(new AgeScope);
}
}
新增作用域後,如果使用 User::all() 查詢則會生成如下 SQL 語句:
select * from `users` where `age` > 200
匿名的全域性作用域
Eloquent 還允許我們使用閉包定義全域性作用域,這在實現簡單作用域的時候特別有用,這樣的話,我們就沒必要定義一個單獨的 Scope 類。
<?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 方法:
// 移除所有全域性作用域
User::withoutGlobalScopes()->get();
//移除某些全域性作用域
User::withoutGlobalScopes([FirstScope::class, SecondScope::class])->get();
本地作用域
本地作用域允許我們定義通用的約束集合以便在應用中複用。
例如,你可能經常需要獲取最受歡迎的使用者,要定義這樣的一個作用域,只需在對應 Eloquent 模型方法前加上一個 scope 字首。
作用域總是返回查詢構建器例項:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 只包含活躍使用者的查詢作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
/**
* 只包含啟用使用者的查詢作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
}
使用本地作用域
作用域被定義好了之後,就可以在查詢模型的時候呼叫作用域方法,但呼叫時不需要加上 scope 字首,你甚至可以同時呼叫多個作用域,例如:
$users = App\User::popular()->active()->orderBy('created_at')->get();
動態作用域
有時候你可能想要定義一個可以接收引數的作用域,你只需要將額外的引數新增到你的作用域即可。作用域引數應該被定義在 $query 引數之後:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 讓查詢只包含給定型別的使用者
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $type
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOfType($query, $type)
{
return $query->where('type', $type);
}
}
現在,你可以在呼叫作用域時傳遞引數了:
$users = App\User::ofType('admin')->get();
事件
Eloquent 模型可以觸發事件,允許你在模型生命週期中的多個時間點呼叫如下方法:retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。
事件允許你在一個指定模型類每次儲存或更新的時候執行程式碼。
retrieved 事件會在從資料庫中獲取已存在模型時觸發。
當一個新模型被首次儲存的時候,creating 和 created 事件會被觸發。
如果一個模型已經在資料庫中存在並呼叫 save 方法,updating/updated 事件會被觸發,無論是建立還是更新,saving/saved 事件都會被觸發。
舉個例子,在 Eloquent 模型中定義一個 $dispatchesEvents 屬性來對映模型生命週期中多個時間點與對應事件類:
<?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 $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
觀察者
如果你在給定模型中監聽多個事件,可以使用觀察者來對所有監聽器進行分組,觀察者類擁有反射你想要監聽的 Eloquent 事件對應的方法名,每個方法接收模型作為唯一引數。
Laravel 並沒有為觀察者提供預設目錄,所以你可以建立任意目錄來存放觀察者類:
<?php
namespace App\Observers;
use App\User;
class UserObserver
{
/**
* 監聽使用者建立事件.
*
* @param User $user
* @return void
*/
public function created(User $user)
{
//
}
/**
* 監聽使用者刪除事件.
*
* @param User $user
* @return void
*/
public function deleting(User $user)
{
//
}
}
要註冊觀察者,可使用你想要觀察模型的 observe 方法,你可以在某個服務提供者的 boot 方法中註冊觀察者。
在本例中,我們在 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()
{
//
}
}