【譯】20個 Laravel Eloquent 小技巧

康彪彪發表於2019-01-24

在其表面簡單易用的機制背後,還有很多半隱藏的功能或者少有人知的方法來實現一些很有用的需求。 在本文中,我將向您展示一些技巧。

1. 增量和減少

如果你平時是這麼做的:

$article = Article::find($article_id);
$article->read_count++;
$article->save();
複製程式碼

那麼你可以試試這樣:

$article = Article::find($article_id);
$article->increment('read_count');
複製程式碼

或者這樣也是可以的:

Article::find($article_id)->increment('read_count');
Article::find($article_id)->increment('read_count', 10); // +10
Product::find($produce_id)->decrement('stock'); // -1
複製程式碼

2. XorY 方法

Eloquent有很多方法是兩個方法的組合,實現 “請做X,否則做Y”這樣的需求。

例 1 findOrFail():

可以把這樣的程式碼:

$user = User::find($id);
if (!$user) { abort (404); }
複製程式碼

換成這樣:

$user = User::findOrFail($id);
複製程式碼

例 2 firstOrCreate():

不需要寫這麼長:

$user = User::where('email', $email)->first();
if (!$user) {
  User::create([
    'email' => $email
  ]);
}
複製程式碼

這樣就夠了:

$user = User::firstOrCreate(['email' => $email]);
複製程式碼

3. 模型的 boot() 方法

在Eloquent模型中有一個名為boot()的神奇地方,您可以在其中覆蓋預設行為:

class User extends Model
{
    public static function boot()
    {
        parent::boot();
        static::updating(function($model)
        {
            // 記錄一些日誌
            // 覆蓋或者重寫一些屬性 比如$model->something = transform($something);
        });
    }
}
複製程式碼

可能最常見的例子之一是在建立模型物件時設定一些欄位值。比方說你需要在建立物件時候生成UUID欄位。

4. 帶條件以及排序的關聯關係模型

通常定義關係模型的方法是這樣的

public function users() {
    return $this->hasMany('App\User');    
}
複製程式碼

但你是否知道在定義關係模型的時候就已經可以增加 where 或者 orderBy 的條件了? 比如說你需要定義一個特定型別的使用者的關聯關係並且用郵箱資訊來排序,那你可以這麼做:

public function approvedUsers() {
    return $this->hasMany('App\User')->where('approved', 1)->orderBy('email');
}
複製程式碼

5. 模型屬性: 時間戳, 附加屬性(appends) 等

Eloquent模型有一些“引數”,會以該類的屬性形式出現。 最常用的可能是這些:

class User extends Model {
    protected $table = 'users';
    protected $fillable = ['email', 'password']; // 這些欄位可以在模型的 create 方法中直接建立
    protected $dates = ['created_at', 'deleted_at']; // 這些欄位將會轉換成 Carbon型別的,可以方便的使用 Carbon 提供的時間方法
    protected $appends = ['field1', 'field2']; // 序列化時候附加的額外屬性,通過模型中定義 getXXXAttribute 的方式來定義
}
複製程式碼

可不僅僅有這些,還有:

protected $primaryKey = 'uuid'; // 模型的主鍵名稱可以不是預設的 id
public $incrementing = false; // 甚至可以不必是自增的型別!
protected $perPage = 25; // 是的,你還定義模型集合分頁引數(預設是 15)
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at'; // 預設的時間戳欄位也是可以改變的
public $timestamps = false; // 或者完全不用他
複製程式碼

甚至還有更多,我僅僅列出了最有意思的一部分,更多請檢視預設抽象Model類的程式碼,並檢視所有使用的trait 方法。

6. 查詢多個實體物件

find()方法想必大家都知道的吧?

$user = User::find(1);
複製程式碼

我很驚訝很少有人知道它可以接受多個ID作為陣列:

$users = User::find([1,2,3]);
複製程式碼

7. WhereX

有一種很優雅的方式可以把下面的程式碼:

$users = User::where('approved', 1)->get();
複製程式碼

改成這樣:

$users = User::whereApproved(1)->get(); 
複製程式碼

是的,你也可以改成任何欄位的名稱,並將其作為字尾附加到“where”,它將神奇的產生預想的效果(通過魔術方法實現呼叫)。

此外,Eloquent中還有一些與日期/時間相關的預定義方法:


User::whereDate('created_at', date('Y-m-d'));
User::whereDay('created_at', date('d'));
User::whereMonth('created_at', date('m'));
User::whereYear('created_at', date('Y'));
複製程式碼

8. 使用關係模型欄位排序

一個更復雜的“技巧”。 如果你有帖子,但要通過最新帖子對它們進行排序? 頂部有最新更新主題的論壇中非常常見的要求,對吧?

首先,定義關於該主題的最新帖子的關係:

public function latestPost()
{
    return $this->hasOne(\App\Post::class)->latest();
}
複製程式碼

接下來可以在我們的控制器中用這個神奇的方法來實現:

$users = Topic::with('latestPost')->get()->sortByDesc('latestPost.created_at');
複製程式碼

9. Eloquent::when() – 不用再寫 if -else 啦

大部分時候我們用 if-else 來實現按條件查詢,類似這樣的程式碼:

if (request('filter_by') == 'likes') {
    $query->where('likes', '>', request('likes_amount', 0));
}
if (request('filter_by') == 'date') {
    $query->orderBy('created_at', request('ordering_rule', 'desc'));
}
複製程式碼

但是一個更好的方法是——使用 when()方法

$query = Author::query();
$query->when(request('filter_by') == 'likes', function ($q) {
    return $q->where('likes', '>', request('likes_amount', 0));
});
$query->when(request('filter_by') == 'date', function ($q) {
    return $q->orderBy('created_at', request('ordering_rule', 'desc'));
});
複製程式碼

它看起來可能不會更短或更優雅,但最強大的是可以傳遞引數:

$query = User::query();
$query->when(request('role', false), function ($q, $role) { 
    return $q->where('role_id', $role);
});
$authors = $query->get();
複製程式碼

10. BelongsTo 關聯的預設模型物件

假設有個 Post(帖子) 物件屬於 Author (作者)物件,在 Blade 模板中有下面的程式碼

{{ $post->author->name }}
複製程式碼

但是如果作者被刪除,或者由於某種原因沒有設定呢? 那麼就會導致報錯,可能是“property of non-object(非物件屬性)”。

當然你可以用下面的程式碼來必變這種錯誤:

{{ $post->author->name ?? '' }}
複製程式碼

不過你可以再模型定義時候就解決這個問題:

public function author()
{
    return $this->belongsTo('App\Author')->withDefault();
}
複製程式碼

在這個例子中,在這個帖子下沒有關聯作者的時候,author()關聯關係將返回一個空的App\Author 模型。

更進一步,我們可以設定一些預設屬性個這個模型。

public function author()
{
    return $this->belongsTo('App\Author')->withDefault([
        'name' => 'Guest Author'
    ]);
}
複製程式碼

11. 自定義屬性排序

假設你有下面的一段程式碼:

(設定了一個在返回物件時候的附加屬性 ‘full_name’參見 tips5 模型屬性: 時間戳, 附加屬性(appends) 等)

function getFullNameAttribute()
{
  return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}
複製程式碼

如果你想要按照 full_name 來排序的話?下面的程式碼是不行的:

$clients = Client::orderBy('full_name')->get(); //不行滴
複製程式碼

當然解決方案也是非常簡單。 我們需要在得到結果以後再對他們進行排序。


$clients = Client::get()->sortBy('full_name'); //穩了
複製程式碼

注意兩個方法名字是不一樣的——不是 orderBy 而是 sortBy

(一個是 SQL 語句,自定義屬性是資料庫沒有的欄位當然不能直接用。但是查詢的返回都是一個 Collection 物件,Laravel 為集合提供了很多方便的操作方法,sortBy 就是其中一個,當然還可以用 filter 等集合操作)

12. 全域性範圍(global scope)內的預設排序

如果你希望User :: all()始終按名稱欄位排序,該怎麼辦? 你可以分配全域性的查詢作用域。 讓我們回到上面已經提到的boot()方法。

protected static function boot()
{
    parent::boot();

    // 預設按照name 欄位升序
    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('name', 'asc');
    });
}
複製程式碼

這裡還有更多關於請求範圍作用域的介紹。

13. 原生查詢方法

有時我們需要在Eloquent語句中新增原生查詢語句。 幸運的是,它提供了這樣的功能。

// 原生 where 語句
$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "TX", ?, 100)', [200])
    ->get();

// 原生 having 語句
Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();

// 原生 orderBy 語句
User::where('created_at', '>', '2016-01-01')
  ->orderByRaw('(updated_at - created_at) desc')
  ->get();
複製程式碼

(本質上Eloquent就是對 DB 查詢物件的一個封裝,所以可以用在 DB 上的原始查詢方法,都可以用在繼承自 Eloquent 的 model 物件上。)

14. 複製: 得到一行資料的一個副本

很簡單的一條,不需要太多解釋。這是生成資料庫條目副本的最佳手段。

$task = Tasks::find(1);
$newTask = $task->replicate();
$newTask->save();
複製程式碼

15. 用於大表大集合的 Chunk()方法

不完全與Eloquent相關,它更多是Collection 集合類提供的方法,但仍然很強大 —— 處理更大的資料集,你可以將它們分成幾塊。

不要這麼做:

$users = User::all();
foreach ($users as $user) {
    // ...
複製程式碼

而是這樣:

User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // ...
    }
});
複製程式碼

類似於資料分片,減少佔用提升效能

16. 在生成模型的時候再額外生成一些模板

我們都知道這個的 Artisan 的命令:

php artisan make:model Company
複製程式碼

但你是否知道它還有三個很有用的引數標記用來生成與這個模型關聯的其他檔案?

php artisan make:model Company -mcr
複製程式碼
  • -m 將會建立模型的遷移(migration)檔案
  • -c 將會建立控制器(contriller)
  • -r 將表用這個控制器應該是一個資源控制器 (resourceful)

17. 在儲存的時候重寫 update_at 欄位

你知道 - > save()方法是可以接受引數的嗎? 因此,我們可以告訴它“忽略” updated_at預設填充當前時間戳的功能。 看這個例子:

$product = Product::find($id);
$product->updated_at = '2019-01-01 10:00:00';
$product->save(['timestamps' => false]);
複製程式碼

這裡我們動態的重寫的 update_at 欄位,而不是預先在模型中定義。

Laravel 預設會給所有實體類配置時間戳,如果不需要一般是在模型中指定 $timestamps = false

18. update()方法的返回值是什麼?

你有沒有曾想過下面這段程式碼返回的 result 是什麼?

$result = $products->whereNull('category_id')->update(['category_id' => 2]);
複製程式碼

我的意思是,更新語句是在資料庫中正確執行的,但 $ result 變數會包含什麼?

答案是受影響的行。 因此,如果您需要檢查受影響的行數,則無需再呼叫任何其他方法 - update()方法將為你返回這個數字。

19. 正確翻譯 SQL 語句中的括號 到 Eloquent 的查詢

假設在你的 SQL 查詢中 包含了 and / or 這樣的關鍵字,如下:

... WHERE (gender = 'Male' and age >= 18) or (gender = 'Female' and age >= 65)
複製程式碼

怎麼翻譯成 Eloquent的查詢呢? 這是錯誤的方法:

$q->where('gender', 'Male');
$q->orWhere('age', '>=', 18);
$q->where('gender', 'Female');
$q->orWhere('age', '>=', 65);
複製程式碼

這個順序是有問題的。正確的方法稍微有些複雜,需要用到閉包函式作為子查詢:

$q->where(function ($query) {
    $query->where('gender', 'Male')
        ->where('age', '>=', 18);
})->orWhere(function($query) {
    $query->where('gender', 'Female')
        ->where('age', '>=', 65); 
})
複製程式碼

20 orWhere方法使用更多引數

最後一條,你可以個 orWhere 方法傳遞一個陣列。

常規用法是:

$q->where('a', 1);
$q->orWhere('b', 2);
$q->orWhere('c', 3);
複製程式碼

你也可以用下面的語句實現一樣的功能:

$q->where('a', 1);
$q->orWhere(['b' => 2, 'c' => 3]);
複製程式碼

相關文章