Những mẹo nhỏ khi làm việc với Laravel Eloquent
Trong quá trình làm việc với Laravel Eloquent ORM, chắc hẳn các bạn từng thực hiện khá nhiều tác vụ lặp đi lặp lại - mà bạn không hề biết Laravel đã hỗ trợ sẵn. Thông qua vài mẹo và thủ thuật nhỏ trong bài viết này, mình hi vọng sẽ giúp các bạn giảm bớt sự phức tạp khi viết code cũng như bớt nhàm chán khi thực hiện các tác vụ lặp đi lặp lại theo cách thông thường.
Tăng hoặc giảm giá trị của một thuộc tính
Mình cá là có khá nhiều người đã từng viết những thứ đại loại như thế này:
$page = Page::find($id);
$page->views_count++;
$page->save();
Cách này hoàn toàn đúng nha các bạn, nhưng mình có thể viết lại như sau:
$page = Page::find($id);
$page->increment('views_count');
Đẹp hơn rồi phải không ^^
Các cách viết như sau cũng hoàn toàn hợp lệ:
Page::find($id)->increment('views_count'); //+1
Page::find($id)->increment('views_count', 100); //+100
Product::find($productId)->decrement('stock'); //-1
Khởi tạo nhanh Model và Migration
Thường khi chúng ta cần tạo ra model và migration, chúng ta sẽ thực hiện tuần tự các lệnh sau:
$ php artisan make:migration create_posts_table --create=posts
$ php artisan make:model Post
Chúng ta chỉ cần thực hiện một trong hai câu lệnh đơn giản hơn sau đây:
$ php artisan make:model Post -migration
$ php artisan make:model Post -m
Các phương thức XorY
Eloquent hỗ trợ một số phương thức kết hợp 2 chức năng, đại loại là "Thực hiện X, ngược lại thực hiện Y".
Khó hiểu quá hả? Vậy chúng ta cùng xem qua một vài ví dụ nhé.
Ví dụ 1: phương thức findOrFail()
Chúng ta có một đoạn code như thế này:
$post = Post::find($id);
if (!$post) {
abort(404);
}
Thay vì viết phức tạp như vậy, chúng ta có thể viết trực tiếp như sau:
$post = Post::findOrFail($id);
Ví dụ 2: phương thức firstOrCreate()
Phàm nhân sẽ viết như thế này:
$user = User::where('email', $email)->first();
if (!$user) {
$user = User::create(['email' => $email]);
}
Chúng ta sẽ thử cách khác xịn xò hơn:
$user = User::firstOrCreate(['email' => $email]);
Ví dụ 3: phương thức insertOrIgnore()
Khi chúng ta thực hiện việc insert một danh sách các bản ghi vào cơ sở dữ liệu, chúng ta thường dùng phương thức insert()
để giải quyết:
Post::insert([
['name' => 'Post 1', 'status' => 'active'],
['name' => 'Post 2', 'status' => 'active'],
]);
Khi chúng ta muốn bỏ qua các bản ghi bị lỗi xảy ra trong quá trình insert:
Post::insertOrIgnore([
['name' => 'Post 1', 'status' => 'active'],
['name' => 'Post 2', 'status' => 'active'],
]);
Phương thức boot()
Chà chà, đây là một nơi kỳ diệu mà Eloquent hỗ trợ chúng ta có thể ghi đè các hành vi mặc định khi thực hiện một số tác vụ nào đó.
Bên dưới là một ví dụ nhỏ về việc thêm một trường uuid
vào bảng tại thời điểm tạo mới một User
.
class User extends Model
{
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->uuid = (string)Uuid::generate();
});
}
}
Cũng khá dễ hiểu đúng không các bạn ^^
Eloquent cũng cung cấp các phương thức obsever khác hỗ trợ cho quá trình thao tác với model như: updating, deleting... Bạn có thể tham khảo liên kết sau để biết thêm nhiều thông tin hơn https://laravel.com/docs/7.x/eloquent.
Thiết lập relationship với các điều kiện và thứ tự sắp xếp
Bên dưới là một ví dụ điển hình cho việc thiết lập relationship giữa 2 model User
và Post
:
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Nhưng nếu như bạn muốn thêm một số điều kiện ràng buộc ở đây, hoặc sắp xếp thứ tự hiển thị của các bài viết thì sao?
Eloquent hỗ trợ thực hiện điều này khá dễ dàng:
public function posts()
{
return $this->hasMany(Post::class)->where('status', 1)->orderByDesc('published_at');
}
Các thuộc tính của Model
Bạn có thể cấu hình một số tham số của Eloquent dưới dạng thuộc tính của lớp. Dưới đây là một số thuộc tính phổ biến mà đa số lập trình viên đều biết:
class User extends Model
{
/**
* Thiết lập tên bảng trong cơ sở dữ liệu của model này
* @var string
*/
protected $table = 'users';
/**
* Thiết lập các trường có thể được fill khi bạn dùng Mass Assignment
* @var array
*/
protected $fillable = [
'first_name',
'last_name',
'uuid',
'status',
'gender',
];
/**
* Ngược lại với $fillable, đây là các trường không thể được fill khi bạn dùng Mass Assignment.
* Chú ý: Eloquent không cho phép bạn đồng thời định nghĩa cả $fillable và $guarded cho model của mình.
* @var array
*/
protected $guarded = [
'id',
];
/**
* Định nghĩa các trường được tự động chuyển sang Carbon object - một class xử lí thời gian mà Laravel đang sử dụng
* @var array
*/
protected $dates = [
'published_at',
];
/**
* Định nghĩa các trường sẽ được thêm vào trực tiếp trong dữ liệu trả về của model thông qua accessors
* @var array
*/
protected $appends = [
'full_name',
];
/**
* Accessor trả về thông tin từ model khi người dùng gọi tới thuộc tính full_name
* @return string
*/
public function getFullNameAttribute()
{
return $this->first_name . ' ' . $this->last_name;
}
}
Nhưng thực ra thì Eloquent hỗ trợ nhiều hơn thế.
/**
* Định nghĩa khóa chính của bảng trong cơ sở dữ liệu
* @var string
*/
protected $primaryKey = 'uuid';
/**
* Đây là các trường lưu lại thông tin thời gian cập nhật dữ liệu của model.
* Bạn có thể cập nhật lại chúng cho phù hợp với cơ sở dữ liệu bạn đang có sẵn.
*/
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* Chỉ định cho model biết có cần thiết lưu thông tin thời gian cập nhật model hay không.
* @var bool
*/
public $timestamps = false;
...
Ở đây mình chỉ nêu một số thuộc tính chúng ta hay sử dụng khi làm việc với model. Bạn có thể tham khảo liên kết sau để biết thêm nhiều thuộc tính khác mà Eloquent hỗ trợ hơn https://github.com/laravel/framework/.../Model.php.
Accessors và Mutators
Trong phần trên mình có nhắc tới Accessor
. Vậy thì Accessor
là gì?
Trong nhiều trường hợp, chúng ta cần truy cập vào một số thuộc tính của model khi mà chúng không thực sự tồn tại trong cơ sở dữ liệu. Ở ví dụ phía trên, chúng ta cần lấy thông tin full_name
của User
để hiển thị cho người dùng. Thay vì phải làm một cái gì đó tương tự như thế này:
{{ $user->first_name . ' ' . $user->last_name }}
Thì chúng ta sẽ khai báo phương thức getFullNameAttribute()
trong Model như ví dụ phía trên, sau đó ở phía người dùng, chúng ta chỉ cần gọi nó ra như sau:
{{ $user->full_name }}
Để khai báo một accessor, bạn tạo ra một phương thức getFooBarAttribute()
trong model của mình, với FooBar
đặt theo chuẩn StudlyCase, còn thuộc tính bạn truy vấn ra sẽ dưới dạng snake_case ($user->foo_bar
).
Giờ code nhìn sạch sẽ hơn rất nhiều rồi phải không nào ^^
Mutators
thì ngược lại với Accessors
, chúng được dùng trong quá trình cập nhật thay vì truy xuất dữ liệu như Accessors
.
Bạn có thể truy cập liên kết sau để tìm hiểu nhiều hơn về Accessors
và Mutators
: https://laravel.com/docs/7.x/eloquent-mutators#accessors-and-mutators.
Phương thức whereX
Có một cách biến đoạn code sau:
$users = User::where('approved', 1)->get();
trở nên dễ hiểu dễ đọc hơn:
$users = User::whereApproved(1)->get();
Đúng thế, bạn chỉ cần thay đổi tên của bất cứ trường nào trong model (foo_bar) và nối nó với tiền tố "where" dưới dạng StudlyCase (FooBar), Eloquent sẽ tự xử lí và khiến đoạn code chạy "chuẩn không cần chỉnh".
Mặt khác, hãy chú ý tới một số phương thức đã được định nghĩa sẵn của 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'));
Post::whereNull('category_id')->get();
Post::whereNotNull('category_id')->get();
Ah ha, sạch sẽ rõ ràng dễ hiểu hơn rất nhiều rồi.
Thực ra thì có khá nhiều người ghét kiểu viết như thế này (trừ những hàm mặc định của Eloquent), nhưng Eloquent đã hỗ trợ và bạn muốn xài thì cứ xài thôi. Trên thực tế mình lại thích viết rõ ràng các tên trường ra thay vì gọi tên hàm, bởi mình thấy nó làm cho code khó tái sử dụng hơn.
Model có quan hệ belongsTo chứa giá trị mặc định
Hãy tưởng tượng bạn có các bài Post
được đăng bởi một Author
. Bạn cần xuất tên của Author
ra ngoài view.
{{ $post->author->name }}
Nhưng điều gì sẽ xảy ra nếu Author
đã bị xóa, hoặc nó chưa được lưu xuống vì một lí do nào đó? Bạn sẽ gặp một lỗi Exception
, đại loại là "try to get property of non-object".
Thực ra bạn có thể tránh điều đó bằng cách sau:
{{ $post->author->name ?? '' }}
Nhưng bạn sẽ phải trả giá bằng cách kiểm tra giá trị như trên ở tất cả những nơi cần chúng. Khá phiền phức đúng không?
Có một cách hay hơn để làm điều này. Chúng ta sẽ thêm đoạn code như sau vào Eloquent model:
public function author()
{
return $this->belongsTo(Author::class)->withDefault();
}
Đoạn code này sẽ trả về một lớp Author
rỗng nếu không có Author
nào đi kèm với bài Post
. Hơn nữa chúng ta hoàn toàn có thể thiết lập các giá trị mặc định cho Author
:
public function author()
{
return $this->belongsTo(Author::class)->withDefault([
'name' => 'Guest',
]);
}
Thiết lập một vài Global Scope
Một ngày đẹp trời, bạn muốn các Category
được sắp xếp theo "name" bất cứ khi nào truy vấn dạng danh sách. Ủa không lẽ giờ ngồi tìm rồi sửa lại hết hay sao ta?
Có một cách để thực hiện điều này tốt hơn, ít "cơ bắp" hơn - chỉ định một Global Scope. Chúng ta cùng quay trở lại phương thức boot()
đã đề cập trước đó:
use Illuminate\Database\Eloquent\Builder;
protected static function boot()
{
parent::boot();
static::addGlobalScope('orderByName', function (Builder $builder) {
$builder->orderBy('name', 'asc');
});
}
Bạn nên tham khảo liên kết sau đây để tìm hiểu rõ hơn về phần Global Scope này https://laravel.com/docs/7.x/eloquent#global-scopes.
Chỉ định các Raw Query
Trong một số trường hợp, khi mà các phương thức xây dựng sẵn của Eloquent không đủ để chúng ta thực hiện truy vấn, chúng ta hoàn toàn có thể thêm vào các raw query.
$orders = Order::whereRaw('price > IF(state = "Texas", ?, 100)', [200])->get();
$products = Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();
$users = User::orderByRaw('(updated_at - created_at) desc')->get();
Replicate - tạo một bản sao của model
Đây là cách ngắn gọn và đơn giản nhất để tạo ra một bản sao cho model:
$post = Post::find($id);
$newPost = $post->replicate();
$newPost->save();
Chunk - xử lí dữ liệu lớn
Khi xử lí hàng ngàn kết quả từ Eloquent, sử dụng chunk
sẽ giúp tiết kiệm tối đa bộ nhớ hệ thống. Thực ra phương thức này thuộc về xử lí Collection
, chứ không hẳn là ở Model
.
Thay vì viết như thế này:
$users = User::all();
foreach ($users => $user) {
// ...
}
Chúng ta cần chuyển sang dạng này:
User::chunk(50, function ($users) {
foreach ($users => $user) {
// ...
}
});
Tạo thêm các cấu trúc liên quan trong lúc khởi tạo một model
Phần phía trên đã đề cập, và hầu như ai trong chúng ta cũng biết câu lệnh này:
$ php artisan make:model Company
Thực ra chúng ta còn có thêm một số tùy chọn khá hữu ích liên quan tới Model:
$ php artisan make:model Company -m -c -r
- -m: tạo kèm một migration tương ứng
- -c: tạo kèm một controller.
- -r: chỉ định controller được tạo ra là resource.
Kết quả trả về của hàm update()
Có bao giờ bạn tự hỏi những đoạn code tương tự như thế này trả về giá trị gì chưa?
$result = Product::whereNull('category_id')->update(['category_id' => 5]);
Câu lệnh này đã thực hiện việc cập nhật category_id
cho toàn bộ các Product
thỏa điều kiện, nhưng giá trị của $result
ở đây là gì?
Câu trả lời chính là: số lượng các mục bị ảnh hưởng. Điều này có nghĩa là nếu bạn muốn kiểm tra xem có bao nhiêu mục đã được cập nhật, thì đây là chính xác những gì bạn cần, không cần gọi thêm thứ gì khác.
Truy vấn điều kiện nâng cao
Nếu bạn có một câu SQL đại loại như thế này:
... WHERE (gender = 'Male' and age >= 20) OR (gender = 'Female' and age >= 18)
Chuyển đoạn SQL này sang Eloquent như thế nào đây? Có lẽ nhiều người từng làm như thế này:
$query->whereRaw('(gender = ? and age >= ?) OR (gender = ? and age >= ?)', ['Male', 20, 'Female', 18]);
Viết như vầy thì "cơ bắp" quá các bạn, cũng khó bảo trì sau này nữa. Trừ trường hợp bất khả kháng, nếu không thì chúng ta nên cố gắng chuyển câu truy vấn sang Eloquent hết sức có thể.
Chúng ta có thể gom nhóm các điều kiện đi kèm với nhau trong dấu ngoặc bằng một hàm Closure
như sau:
$query
->where(function ($q) {
$q->where('gender', 'Male')->where('age', '>=', 20);
})
->orWhere(function ($q) {
$q->where('gender', 'Female')->where('age', '>=', 18);
});
Phương thức orWhere với nhiều tham số
Đôi khi chúng ta cần truy vấn dữ liệu với nhiều điều kiện OR
. Đây là cách thông thường mà mọi người hay sử dụng:
$query
->where('category_id', $categoryId)
->orWhere('name', $name)
->orWhere('description', $description)
->get();
Ổn và không có vấn đề gì cả. Nhưng chúng ta có thể rút gọn nó như sau:
$query
->where('category_id', $categoryId)
->orWhere(['name' => $name, 'description' => $description])
->get();
Trên đây là những kinh nghiệm và mẹo nhỏ mà mình tìm hiểu được khi làm việc với Laravel. Nếu bạn biết một vài mẹo nào đó và muốn chia sẻ, hãy để lại bình luận bên dưới nhé.
Cám ơn các bạn đã theo dõi. Hẹn gặp lại trong các bài viết tiếp theo.