Thủ thuật Laravel Eloquent ORM

10 min read

Eloquent ORM trong Laravel thoạt nhìn có vẻ như đơn giản nhưng ẩn sâu dưới lớp vỏ bọc đó là nhiều chức năng bí ẩn nhưng lại ít được biết đến. Trong bài viết này mình sẽ giới thiệu đến các bạn 20 trong số những chức năng thú vị nhưng mạnh mẽ đó.

1. Increments and Decrements

Khi chúng ta cần increments một trường của một record, có thể bạn sẽ sử dụng một đoạn code như thế này:

$article = Article::find($article_id);
$article->read_count++;
$article->save();

Bạn có thể thay nó bằng đoạn code sau:

$article = Article::find($article_id);
$article->increment('read_count');

Ngoài ra, mọi thứ cũng sẽ hoạt động tốt nếu như bạn sử dụng:

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 methods

Eloquent có khá nhiều chức năng kết hợp hai phương thức. Có thể hiểu đơn giản nó như là: "hãy làm cho tôi việc X nhé, nếu không được thì làm Y giúp tôi".

Ví dụ 1- findOrFail():

Thay vì sử dụng:

$user = User::find($id);
if (!$user) { abort (404); }

Hãy làm đơn giản hơn:

$user = User::findOrFail($id);

Chắc hẳn phương thức trên cũng đã quá quen thuộc với chúng ta rồi.

Ví dụ 2 - firstOrCreate():

Khi bạn cần tìm một record có thuộc tính thỏa mãn một điều kiện, nếu không tìm thấy thì sẽ tạo mới một record với chính thuộc tính đó:

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

Mọi thứ chạy tốt và cũng dễ hiểu nhưng chúng ta có thể làm ngắn hơn:

$user = User::firstOrCreate(['email' => $email]);

3. Model boot() method:

Trong mô hình Eloquent, khi bạn muốn override những phương thức mặc định của một model, hãy làm nó ở trong boot():

class User extends Model
{
    public static function boot()
    {
        parent::boot();
        static::updating(function($model)
        {
            // override some logging
        });
    }
}

Có lẽ một trong những ví dụ phổ biến nhất là khi bạn muốn đặt giá trị cho một trường nào đó tại thời điểm tạo mới một model object:

public static function boot()
{
  parent::boot();
  self::creating(function ($model) {
    $model->uuid = (string)Uuid::generate();
  });
}

4. Relationship with conditions and ordering

Đây là cách mà chúng ta thường dùng để định nghĩa một quan hệ:

public function users() {
    return $this->hasMany('App\User');    
}

Nhưng bạn có biết chúng ta cũng có thể tạo ra một quan hệ khác bằng việc kết hợp thêm điều kiện vào quan hệ ban đầu hay không?

Cụ thể, nếu như bạn muốn có một quan hệ chỉ bao gồm các users có trạng thái approved và được order theo email thì bạn có thể làm như sau:

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

5. Model properties: timestamps, appends etc.

Có một vài tham số của một Eloquent model, chúng được thể hiện dưới dạng các thuộc tính trong class. Các tham số phổ biến nhất có lẽ là:

class User extends Model {
    protected $table = 'users';
    protected $fillable = ['email', 'password']; // which fields can be filled with User::create()
    protected $dates = ['created_at', 'deleted_at']; // which fields will be Carbon-ized
    protected $appends = ['field1', 'field2']; // additional values returned in JSON
}

Nhưng cũng còn nhiều thứ khác nữa:

protected $primaryKey = 'uuid'; // it doesn't have to be "id"
public $incrementing = false; // and it doesn't even have to be auto-incrementing!
protected $perPage = 25; // Yes, you can override pagination count PER MODEL (default 15)
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at'; // Yes, even those names can be overridden
public $timestamps = false; // or even not used at all

Đó là những thuộc tính theo tôi là thú vị nhất. Để tìm hiểu thêm các bạn có thể kiểm tra code của các abstract Model class và xem chúng đã sử dụng những gì.

6. Find multiple entries

Chắc hẳn ai cũng đã từng sử dụng method find() rồi phải không?

$user = User::find(1);

Nhưng tôi không chắc là có nhiều người biết nó cũng có thể nhận vào nhiều ID như là một mảng:

$users = User::find([1,2,3]);

7. WhereX

Chắc hẳn chúng ta đã thường xuyên làm điều tương tự như thế này:

$users = User::where('approved', 1)->get();

Và đây là những gì bạn cũng có thể làm:

$users = User::whereApproved(1)->get();

Đúng vậy, bạn có thể kết hợp bất kỳ tên trường nào với tiền tố where và chúng sẽ làm việc như những gì bạn mong muốn.

Bạn cũng có thể sử dụng những method đã được định nghĩa từ trước liên quan đến date/time:

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. Order by relationship

Một thủ thuật phức tạp hơn. Bạn sẽ làm gì nếu như bạn có một diễn đàn với nhiều topics nhưng bạn lại muốn order chúng dựa vào bài viết mới nhất. Đó cũng là một yêu cầu khá phổ biến trong các diễn đàn khi các topics có những bài viết được cập nhật mới nhất sẽ được đưa lên đầu.

Đầu tiên chúng ta hãy tạo ra một quan hệ giữa post mới nhất và topic:

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

Tiếp theo, ở trong controller chúng ta thực hiện một phép thuật:

$topics = Topic::with('latestPost')->get()->sortByDesc('latestPost.created_at');

9. Eloquent::when() – no more if-else’s

Nhiều người trong chúng ta đã từng viết các câu queries phụ thuộc vào điều kiện được đưa vào. Nó có thể trông giống thế này:

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'));
}

Nhưng có một cách tốt hơn đó là sử dụng 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'));
});

Nó có thể không ngắn hơn nhưng sẽ rất hiệu quả nếu như chúng ta đưa vào các tham số:

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

10. BelongsTo Default Models

Hãy tưởng tượng bạn có các bài viết thuộc về một tác giả nào đó, ở ngoài view chúng ta có đoạn code sau:

{{ $post->author->name }}

Nhưng sẽ thế nào nếu như author của post đó bị xóa hay không được set vì một lý do nào đó. Bạn sẽ gặp những thông báo lỗi tương tự như "property of non-object". Tất nhiên là chúng ta có thể ngăn chặn bằng cách sử dụng:

{{ $post->author->name ?? '' }}

Nhưng bạn cũng có thể làm điều đó ở tầng Eloquent:

public function author()
{
    return $this->belongsTo('App\Author')->withDefault();
}

Quan hệ author() lúc này sẽ trả về một empty object của App\Author trong trường hợp author của post không tồn tại. Hoặc chúng ta cũng có thể gán những giá trị mặc định cho empty object đó:

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

11. Order by Mutator

Hãy tưởng tượng bạn có đoạn code này:

function getFullNameAttribute()
{
  return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}

Và bây giờ bạn muốn order theo full_name. Điều đó là không thể

$clients = Client::orderBy('full_name')->get(); // doesn't work

Giải pháp cho vấn đề này vô cùng đơn giản. Chúng ta sẽ order kết quả sau khi đã lấy chúng ra từ database:

$clients = Client::get()->sortBy('full_name'); // works!

Sự khác nhau giữa orderBysortByorderBy thực hiện order ở cấp query trong khi sortBy thực hiện nó ở tầng object.

12. Default ordering in global scope

Điều gì sẽ xảy ra khi bạn muốn mỗi lần gọi User::all() chúng ta sẽ được một danh sách các users đã được order theo name. Hãy cùng trở lại với phương thứcboot() mà mình đã giới thiệu với các bạn ở phần trước.

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

    // Order by name ASC
    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('name', 'asc');
    });
}

Tìm hiểu thêm về Query Scopes tại đây.

13. Raw query methods

Đôi khi bạn cần viết một câu query thuần. May mắn là Eloquent cung cấp cho chúng ta rất nhiều function để làm điều đó:

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

// havingRaw
Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();

// orderByRaw
User::where('created_at', '>', '2016-01-01')
  ->orderByRaw('(updated_at - created_at) desc')
  ->get();

14. Replicate: make a copy of a row

Có lẽ không cần phải giải thích gì nhiều, cách tốt nhất để tạo ra một bản sao của một model object là:

$task = Tasks::find(1);
$newTask = $task->replicate();
$newTask->save();

15. Chunk() method for big tables

Không hẳn là liên quan đến Eloquent, nó có vẻ liên quan đến Collection nhiều hơn, nhưng vẫn rất hữu ích khi bạn cần xử lý một lượng lớn dữ liệu. Bạn có thể tách chúng ra thành từng phần nhỏ.

Thay vì thực hiện:

$users = User::all();
foreach ($users as $user) {
    // ...
}

Chúng ta sẽ làm điều này:

User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // ...
    }
});

16. Create additional things when creating a model

Chúng ta đều đã quá quen thuộc với Artisan command này:

php artisan make:model Company

Nhưng bạn có biết rằng còn có 3 flags hữu dụng khác nữa liên quan đến việc tạo mới model đó ?

php artisan make:model Company -mcr

Trong đó:

  • m sẽ tạo file migration

  • c sẽ tạo controller tương ứng

  • r sẽ thông báo rằng controller là một resourceful

17. Override updated_at when saving

Bạn có biết rằng phương thức save() có thể nhận thêm parameters. Thực tế là chúng ta có thể bỏ qua việc thay đổi trường updated_at - một chức năng mặc định được thực hiện bất chứ khi nào một record trong database có thay đổi.

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

Ở đây chúng ta đã tự thay đổi trường updated_at theo ý mình.

18. What is the result of an update()?

Đã bao giờ bạn tự hỏi thứ gì sẽ được trả về sau khi chạy đoạn code này hay không?

$result = $products->whereNull('category_id')->update(['category_id' => 2]);

Ý tôi là đoạn code trên sẽ thực hiện việc update nhưng sau đó thì chúng ta sẽ nhận lại được gì? Câu trả lời là số dòng trong database bị ảnh hưởng sau câu lệnh update. Nếu như bạn cần kiểm tra xem đã có bao nhiêu dòng bị ảnh hưởng, bạn sẽ chẳng phải làm gì khác nữa cả vì phương thức update() đã trả lại con số đó giúp bạn rồi.

19. Transform brackets into an Eloquent query

Điều gì sẽ xảy ra khi trong câu query của bạn có các toán tử and-or kết hợp với nhau:

... WHERE (gender = 'Male' and age >= 18) or (gender = 'Female' and age >= 65)

Làm thế nào để chuyển nó về dạng của Eloquent? Đây là một cách làm sai:

$q->where('gender', 'Male');
$q->orWhere('age', '>=', 18);
$q->where('gender', 'Female');
$q->orWhere('age', '>=', 65);

Thứ tự các câu query là không chính xác. Cách làm đúng đắn sẽ hơi phức tạp một chút, đó là sử dụngclosure functions giống như sub-queries:

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

20. orWhere with multiple parameters

Điều cuối cùng mà mình muốn chia sẻ với các bạn đó là việc truyền một array vào trong phương thức orWhere(). Đây là cách chúng ta thường làm:

$q->where('a', 1);
$q->orWhere('b', 2);
$q->orWhere('c', 3);

Thay vào đó bạn có thể làm điều này:

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

Summary

Như vậy mình đã giới thiệu tới các bạn tips trong Laravel. Hi vọng bài viết sẽ giúp các bạn biết thêm về những chức năng mạnh mẽ ẩn giấu trong Eloquent cũng như việc hiểu rõ chúng để có thể sử dụng chúng hiệu quả hơn.

0
Subscribe to my newsletter

Read articles from directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by