+42

Tối ưu lại code Laravel của chính mình(P2)

Tiếp nối thành công từ bài viết lần trước từ ... 3 năm trước khi nhận được khá nhiều sự quan tâm của mọi người. Trong bài viết này mình tiếp tục khai thác một số đoạn code khá thối và refactor chúng.

Nếu trong bài viết trước mình nêu ra cách tối ưu về cách triển khai syntaxconvention thì ở bài viết này mình sẽ khai thác về khía cạnh performance và viết code sao cho dễ triển khai unit test.

Nếu là người ngại đọc những thứ lan man thì có thể lướt qua những nội dung chính trong bài mình tổng hợp như sau

  • Đừng lạm dụng destroy để xóa nhiều bản ghi một lúc vì chúng sẽ tốn nhiều query
  • Không dùng helper vì nó sẽ khiến bạn viết unit test khó hơn
  • Đừng gọi thằng biến env trong code nếu muốn dùng config:cache
  • Hạn chế dùng whereHas nếu không muốn hệ thống của bạn chậm như rùa

1. Cân nhắc khi xóa bản ghi

Trong Laravel mình thấy mọi người thường dùng 3 hàm để xóa một bản ghi (nếu có cách khác vui lòng comment cho mình biết thêm)

//Sytaxt
User::where('id', $id)->delete();
//Syntax
$user = User::find($id); 
$user->delete();
//Syntax
User::destroy($id); //$id là một array

Trong khi code dự án, mình có một task phải xóa nhiều bản ghi trong một action. Chúng ta có 2 cách để thực hiện chúng với tư duy của một lập trình viên biết sql

// Cách 1
DELETE FROM table WHERE id = 1;
DELETE FROM table WHERE id = 4;
DELETE FROM table WHERE id = 6;
DELETE FROM table WHERE id = 7;
// Cách 2 
DELETE FROM table WHERE id IN (1, 4, 6, 7)

Trước đây mình hay sử dụng hàm destroy() cho tiện vì cú pháp hết sức đơn giản. Nhưng chính vì nó quá tiện nên chúng ta đã rơi vào cái bẫy của Laravel. Trước khi đọc code base của Laravel, mình cứ nghĩ hàm này sẽ dùng WHERE IN để tìm các bản ghi rồi thực hiện việc xóa các bản ghi để tiết kiệm truy vấn.

Nhưng nhìn qua lại một chút cách mà Laravel đã xây dựng hàm destroy()

    public static function destroy($ids)
    {

       ... Còn một số đoạn code nữa nhưng dài quá không show ra  

        // We will actually pull the models from the database table and call delete on
        // each of them individually so that their events get fired properly with a
        // correct set of attributes in case the developers wants to check these.
        $key = ($instance = new static)->getKeyName();

        $count = 0;

        foreach ($instance->whereIn($key, $ids)->get() as $model) {
            if ($model->delete()) {
                $count++;
            }
        }

        return $count;
    }

Các bạn nếu để ý kĩ thì thấy Laravel đã làm theo tư duy của cách 1. Đầu tiên mình cũng phải thốt lên, Laravel thật ngok ngeck. Và mình cũng nghĩ trong trường hợp trên mình sử dụng hàm delete() của Eloquent Builder sẽ tốn ít query hơn.

User::whereIn('id', $id)->delete();

Nhưng khoan vội phán xét =)) thực ra họ có lí do cả đấy.

Việc họ tiến hành xóa bản ghi một cách máy móc để đảm bảo có thể mỗi khi xóa bản ghi có thể kích hoạt các model event như deleting, deleted đối với từng bản ghi(record).

Chốt lại vấn đề ở đây chúng ta có thể nhìn nhận khách quan về việc xóa nhiều bản ghi như sau.

1. Nếu bạn không cần trigger các model event. Hãy sử dụng whereIn rồi dùng delete() của Query Builder.
2. Nếu bạn cần trigger model event, Bạn không thể dùng hàm delete() của Query Builder vì nó không kích hoạt sự kiện của model event, destroy() sẽ là một trong số cách lựa chọn của bạn, tuy nhiên bạn sẽ đối mặt với vấn đề có nhiều truy vấn sql trong một action một cách không cần thiết

2. Cân nhắc trước khi dùng helper

Về bản chất thì Laravel có hai loại helpersSupport HelpersFoundation Helpers

Foundation Helpers: cung cấp những chức năng với input và output đơn giản theo hướng hàm. Ví dụ như Log, Str, Collection

Support Helpers: cung cấp những api từ core classes của Laravel, nó chỉ là "mặt nạ" tham chiếu đến một instance trong container

Chúng ta cùng đi vào ví dụ với 2 loại này.

//Support helpers
logger('Debug message');
// Foundation Helpers
Log::info('Debug message');

Bản chất thì cả 2 đều tham chiếu đến đối tượng \Illuminate\Log\LogManager. Tuy nhiên khi viết unit test chúng ta sẽ thấy việc dùng Support Helpers không hề có lợi cho lập trình viên.

Nếu sử dụng logger() thì mình phải hiểu bản chất rằng LogManager được binding thế nào trong container để mock một đối tượng với key phù hợp. Còn khi sử dụng Log facade, mình có thể mock "trực tiếp" từ facade theo sự hỗ trợ từ Laravel.

Unit test với Support helpers

$logger = m::mock('Illuminate\Log\LogManager')->shouldReceive('info')->....;
// Vì logger được bind bằng một key là "log" nên muốn giả lập thì phải bind lại nó trong container.
app()->instance('log', $logger);

// run code to test

Unit test với Foundation Helpers

Log::shouldReceive('info')->....

// run code to test

Chốt lại vấn đề là nếu dự án có viết Unit test, thì nên cân nhắc việc ngừng sử dụng Support helpers. Vì lúc viết test, phải tìm rất kĩ mới thấy đối tượng để tiến hành mock.

3. Sử dụng biến env

Không chỉ Laravel (PHP) mà với tất cả các loại ngôn ngữ lập trình, dự án, không nên sử dụng trực tiếp biến môi trường trong source code mà chỉ thao tác với cấu hình.

Trong docs của Laravel cảnh báo về điều này.

Nếu bạn thực thi "config:cache" trong khi quá trình deploy, bạn nên chắc chắn rằng bạn chỉ gọi hàm env từ những files cấu hình của mình. Một khi cấu hình được cached, file .env sẽ không được load và tất cả kết quả gọi đến env function sẽ trả về null

Vậy muốn sử dụng các biến được lưu trong file .env của dự án thì ta phải làm như thế nào ? câu trả lời là cho vào config

// bad
if (env('APP_ENV') !== 'local') {
    URL::forceScheme('https');
}
// good
config/app.php
'env' => env('APP_ENV', 'production'),
logic code
if (config('app.env') !== 'local') {
    URL::forceScheme('https');
}

4. Hạn chế dùng whereHas

Từng làm qua nhiều dự án Laravel, mỗi lần điều tra nguyên nhân query chậm là lại thấy sự xuất hiện của whereHas, có rất nhiều issue liên quan đến việc này được đề cập đến như #18415, #3543.

Vậy giải pháp ở đây là không dùng whereHas nữa là xong =)) Nhưng vì là một người viết bài có tâm nên mình xin phép giải thích sự hoạt động của whereHas để giải thích tại sao nó lại chậm để chúng ta cùng hiểu rõ hơn nhé

Replay::whereHas('players', function ($query) {
    $query->where('battletag_name', 'test');
})->get();

Khi chúng ta log query này ra sẽ nhận được câu lệnh sql như sau

select * from `replays` 
where exists (
    select * from `players` 
    where `replays`.`id` = `players`.`replay_id` 
      and `battletag_name` = 'test') 
order by `id` asc 
limit 100;

Chúng ta có thể thấy khi dùng whereHas chúng ta đã sử dụng subqueries để truy vấn đến bảng players thay vì dùng joins. Vì vậy thời gian truy vấn sẽ chậm hơn, trên viblo đã có một bài viết so sánh rất hay về subqueries và join của anh Đinh Hoàng Long, các bạn có thể tham khảo thêm.

Vậy có phải Laravel lại ngok ngeck khi dùng subqueries hay không ? Tại sao không dùng join để truy vấn nhanh hơn ? Chúng ta cùng đi qua một số ưu điểm mà subqueries mang lại:

  • Độ phức tạp ít hơn
  • Dễ hiểu, dễ viết
  • Tách biệt logic

Vì những ưu điểm trên mà subqueries luôn là lựa chọn hàng đầu của framework hay ORMs. Quay lại chủ đề chính, nếu trường hợp dùng whereHas thì refactor như thế nào? Dùng join là một lựa chọn

// Bad
Replay::whereHas('players', function ($query) {
    $query->where('battletag_name', 'test');
})->get();
// Good
Replay::join('players', 'players.replay_id', '=', 'replays.id')->where('players.battletag_name', 'test')->get()

5.Tổng kết

Nếu bạn là người lười đọc, chỉ cần nhớ 4 nội dung quan trọng sau

  • Đừng lạm dụng destroy để xóa nhiều bản ghi một lúc vì chúng sẽ tốn nhiều query
  • Không dùng helper vì nó sẽ khiến bạn viết unit test khó hơn
  • Đừng gọi thằng biến env trong code nếu muốn dùng config:cache
  • Hạn chế dùng whereHas nếu không muốn hệ thống của bạn chậm như rùa

Cảm ơn các bạn đã theo dõi bài viết, nếu bài viết hữu ích vui lòng upvote và follow để có mình có nhiều động lực viết bài hơn 😄

6. Tham khảo

Ngoài ra bài viết là sự học hỏi của mình trong quá trình làm việc tại công ty, được giúp đỡ rất nhiều của các anh em giúp mình có thêm nhiều kiến thức hơn trong quá trình chia sẻ, chứ không phải kiến thức sâu xa hay nghiên cứu trong sách vở nào cả.

Đọc những bài viết khác của tác giả: Chillwithsu.com


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí