Laravel TDD Unit Testing

Laravel Eloquent và những cái best practices

Hi các bạn,

Xài Laravel với Eloquent thì cũng như là ăn bánh mì phải có pate. Hai combi không thể thiếu.

Mà để tận dụng tốt khả năng của Eloquent, chúng ta phải tìm đến những Best Practices.

Cùng dạo qua những cái Best Practices nào.

1/ Sử dụng created_at / updated_at built-in của Eloquent

Record sinh ra khi nào và sửa lúc nào, ta đều phải log lại hết. Built-in của Eloquent giúp chúng ta làm điều đó cực kỳ đơn giản.

Chỉ đơn giản là lúc tạo 1 Migration mới, “$table->timestamps()” là xong :))

2/ Sử dụng deleted_at / Soft Deletes của Eloquent

Dành cho những tables mà data rất là quan trọng, ai đời lại đi hard delete các bạn nhỉ?

Để tìm hiểu về Soft Deletes, mình có viết 1 bài ở đây:

Laravel Eloquent: Soft Delete

====

Note, các phần dưới đây, tôi đoán dc là bạn đã biết về Eloquent Relationships. Nếu chưa hãy visit trang này nhé:

Laravel 5 Eloquent Relationship căn bản

====

3/ Eager Loading before Accessing

Như các bạn đã biết về Eloquent Relationships – chúng ta sẽ define các relationships thông qua Eloquent Entity.

Tuy nhiên, Eloquent thông minh đến nỗi, mọi Relationship Access sẽ được lazy-load (chỉ load/Query database khi bạn thực sự access vào).

Vậy nên, problems ở đây: Với 1 bài viết có 100 comments, relationship là 1 article – n comments thì ta hay dùng như sau:

foreach ($article->comments as $comment) {
...
}

Và thực tế Query sẽ chạy là:

SELECT * from `article` where `id` = ...;

SELECT * from `comment` where `id` = ...
SELECT * from `comment` where `id` = ...
SELECT * from `comment` where `id` = ...
SELECT * from `comment` where `id` = ...
....

Có nghĩa là, độ phức tạp của nó đang là O(n+1)

Vậy nên, để giảm thiểu dc vụ này, ta sẽ cần Eager Loading của Laravel, sử dụng đơn giản như sau với method with:

$article = Article::with(['comments'])->find($id);

Queries Eloquent sẽ generate ra là:

SELECT * from `article` WHERE `id` = ...;

SELECT * from `comments` WHERE `article_id` = ...;

Rất đơn giản phải ko nào? Problems của chúng ta đã dc giảm xuống còn O(2)

4/ Depth-Accessing Eager Loading

Với ví dụ ở trên, nếu tôi muốn lấy users đã posted comment(s) thì sao?

Đơn giản, với dot notation, bạn có thể làm việc đó dễ dàng như sau:

Article::with([
	'comments',
	'comments.user'
])
->find($id);

Rất đơn giản phải không nào, giờ thì cùng access như sau:

foreach ($article->comments as $comment) {
	$user = $comment->user; // usable now
}

Hehehe

5/ With with special actions

Giả sử bạn muốn sort (orderBy) những cái comments theo ngày dc đăng mới nhất hay là 1 condition nào đó.

Hoặc chỉ select những columns nhất định (nhẹ bộ nhớ), thì ta sẽ đơn giản làm việc như sau, tạo ra 1 closure và thêm query thoy:

Article::with([
	'comments' => function($qComment) {
		$qComment->select(['id', 'user_id', 'created_at', 'comment']);
		$qComment->orderBy('created_at', 'DESC');
	}
])
->find($id);

Và thế, comments chỉ lấy ra id,user_id,comment,created_at cũng như là sort theo ý bạn muốn.

Đây là 1 best practices khá tốt, không nên select * ra hết vì chắc gì bạn đã xài hết chúng đâu 😀

6/ whereHaswhereDoesntHave Pro MAX

With cũng như Join, là các bạn sẽ ra những điều kiện gì bạn muốn join vào.

Where sẽ là nơi bạn phân loại/filter data khi đã joined tất cả.

whereHas hay `whereDoesntHave` là 2 methods dùng để where trên relationships.

Và 2 methods này cũng có thể dùng dot notation để các bạn có thể where trên 1 relationship nhất định 😀

// vd, get comments of a specific user
Article::whereHas('comments', function($q) {
	$q->where('user_id', Auth::user()->id);
});

7/ Viết scope viết scope và viết scope

Scope là một cách tạo ra những set conditions nhất định giúp chúng ta query nhanh gọn hơn.

Đọc thêm về scope: https://laravel.com/docs/8.x/eloquent#query-scopes

Ví dụ, bạn muốn lấy ra tất cả comments của 1 user nào đó,  thông thường bạn sẽ viết Query = Eloquent như sau:

$commentsOfUser = Comment::where('user_id', $userId)->get();

Nhưng nếu có scope, nó sẽ như sau:

// in Comment.php
public function scopeByUser($query, $userId) {
    return $query->where('user_id', $userId);
}

// in your app
$commentsOfUser = Comment::byUser($userId)->get();

Elegant hơn, phải không? Ngoài ra nó còn có 1 opportunity nữa là những gì related tới database, bạn sẽ để trong Model file.

8/ Tận dụng tối đa Eloquent Accessor

Assessor (hay còn dc gọi là Getter) là một feature built-in trong Eloquent ORM. Nó giúp chúng ta format raw data từ database thành một formatted-data theo ý ta muốn.

Tận dụng dc Accessor sẽ giúp ích được mình rất nhiều từ công đoạn develop tới maintain.

Ví dụ, access lấy tên của user trong comment với cách thông thường:

foreach ($article->comments as $comment) {
	$userName = $comment->user->name;
}

Thông qua accessor:

// in Comment.php
public function getCommentedUserNameAttribute() {
    return $this->user->name;
}

// In your code
$userName = $comment->commentedUserName;

Cool, right?

Thêm các ví dụ tiếp theo như sau:

// in Article.php
public function getcountAllCommentsAttribute() {
    return $this->comments->count();
}

// in User.php
public function getNameAttribute() {
    return "{$this->first_name} {$this->last_name}";
}

//...

Bạn có thể đọc thêm về Accessor tại đây: https://laravel.com/docs/8.x/eloquent-mutators

9/ Data-Casting

Eloquent có đi kèm Data-Casting cho các bạn sử dụng (ép kiểu). Với data-casting, những field nào có những data-type nào bạn muốn, đơn giản là define vào $cast trong Eloquent Model:

Ngoài ra, nếu bạn có thêm date field nào nữa (ngoài created_at, updated_at, deleted_at) thì bạn cũng nên define casting, Eloquent sẽ tự động parse date từ database vào tạo ra 1 Object Carbon, từ đó giúp bạn làm việc với Date/DateTime/Timestamp thoải mái hơn.

Đọc thêm tại: https://laravel.com/docs/8.x/eloquent-mutators#attribute-casting

10/ Dynamic Appends

$appends là một configuration mà khi chúng ta convert Eloquent qua Assoc-Array hay JSON-string, nó sẽ tự động kèm theo Accessor mà chúng ta đã tạo ra.

Mặc định thì Eloquent sẽ không thêm bất kỳ Accessor nào của bạn vào converted data.

Cách thông thường ta hay define:

class Comment extends Model {
    //...

    protected $appends = [
        'commentedUserName',
        'formattedCreatedAt',
        //...
    ];

}

Bình thường là vậy, nhưng nó ko tốt chút nào, bởi vì khi càng develop lâu dài, kiểu gì cũng sinh ra nhiều Accessors, rồi lại càng nhiều append attributes.

Và tất nhiên, không phải lúc nào ta cũng xài hết tất cả các accessors (nhất là API request), nó phát sinh ra vấn đề dư thừa dữ liệu, vừa tốn performance mà cũng tốn space.

Để tránh dc trường hợp này, ta cần phải dùng setAppends:

$article->setAppends([
    'countAllComments'
]);

foreach ($article->comment as $comment) {
    $comment->setAppends([
        'commentedUserName',
        'formattedCreatedAt', 
    ]);
}

$article->toJSON();

Đơn giản nhưng lại là 1 practice good!

Conclusion

Một dự án mà đã xác định đi lâu và dài, thì các bạn nên chú tâm tìm best practices xung quanh framework & libraries mà bạn xài để các bạn có thể tận dụng hết:

  • Feature
  • Upgrade
  • Maintain

Chúc các bạn code vui vẻ =))

facebook
Seth Phát

Seth Phát

Mình là Phát - biệt danh Seth Phát. Hiện đang là một Sr. Full-Stack Engineer. Mình là một người yêu thích và đam mê lập trình và hiện tại đang theo về phần Web là chủ yếu. Mạnh Back-end và khá Front-end, vẫn đang theo đều cả 2 :v. Còn gì bằng khi được làm những thứ mà mình yêu thích, đam mê ;)

Leave a Reply

Your email address will not be published. Required fields are marked *

Bình luận qua Facebook