Series Nâng cao Laravel Query Builder #1: Cởi bỏ gánh nặng ORM và Tuyệt kỹ Xử lý Dữ liệu Khổng lồ
1. Ảo tưởng của ORM và Sự thật về Performance
Eloquent ORM của Laravel rất tuyệt vời. Nó cung cấp các tính năng Mutators, Accessors, Relationships (Eager Loading) giúp code sạch và dễ bảo trì.
Tuy nhiên, có một sự thật tàn khốc: Mỗi khi bạn lấy ra một bản ghi bằng Eloquent, Laravel phải khởi tạo một Object (Đối tượng) đầy đủ trong bộ nhớ RAM.
Nếu bạn gọi lệnh: Transaction::where('status', 'success')->get(); để lấy ra 100.000 giao dịch. Laravel sẽ tạo ra 100.000 Objects (chứa đầy đủ các methods, hidden attributes, original states...). RAM của Server sẽ bị "thổi bay" hàng trăm Megabytes chỉ trong 1 giây, dẫn đến lỗi Allowed memory size exhausted.
Giải pháp của Senior: Trở về với Query Builder
Khi bạn chỉ cần ĐỌC dữ liệu để xuất báo cáo (Excel, CSV) hoặc làm API trả về danh sách thô, hãy dùng Query Builder:
use Illuminate\Support\Facades\DB;
// Query Builder trả về một Collection chứa các đối tượng stdClass chuẩn của PHP
// Tốc độ nhanh gấp 2-3 lần và tiêu tốn cực ít RAM so với Eloquent
$transactions = DB::table('transactions')
->where('status', 'success')
->get(['card_uid', 'station_id', 'amount', 'created_at']);
2. Kế hoạch Giải cứu RAM: Chunking và Cursor
Dù Query Builder đã nhẹ hơn Eloquent, nhưng việc get() ra 1 triệu bản ghi từ Database cùng một lúc vẫn sẽ làm cháy RAM. Đây là lúc bạn phải dùng đến các kỹ thuật chia nhỏ dòng chảy.
Cách 1: Chunk (Cắt khúc) Thay vì bê 1 cục 1 triệu dòng, chunk() sẽ phát sinh các câu lệnh LIMIT và OFFSET ngầm định để lấy từng nhóm nhỏ.
DB::table('transactions')->orderBy('id')->chunk(1000, function ($transactions) {
// Xử lý 1000 bản ghi mỗi vòng lặp
foreach ($transactions as $transaction) {
// Đồng bộ lên hệ thống Hitachi...
}
});
Lưu ý tử huyệt: Không bao giờ dùng chunk() nếu bên trong vòng lặp bạn thực hiện thao tác UPDATE làm thay đổi điều kiện WHERE của câu query ban đầu. Việc này sẽ làm hỏng OFFSET và khiến dữ liệu bị lọt sổ. Trong trường hợp đó, hãy dùng chunkById().
Cách 2: Tuyệt kỹ Cursor (Con trỏ Generator) Nếu chunk vẫn tiêu tốn RAM cho 1000 bản ghi mỗi lần, thì cursor() là đỉnh cao của tối ưu. Nó sử dụng tính năng Generators (yield) của PHP. Nó chỉ giữ đúng 1 bản ghi duy nhất trên RAM tại một thời điểm!
// Dù có 10 triệu dòng, RAM tiêu thụ vẫn chỉ ở mức vài Megabytes
$transactions = DB::table('transactions')->where('sync_status', 'pending')->cursor();
foreach ($transactions as $transaction) {
// Xử lý tuần tự từng dòng một
}
3. Upsert: Tuyệt kỹ Bulk Insert & Cập nhật Đồng loạt
Hãy tưởng tượng một luồng dữ liệu 5000 mã sản phẩm mới đẩy về, bạn cần: Thêm mới nếu chưa có, hoặc Cập nhật giá/tồn kho nếu đã tồn tại.
Một Junior Backend sẽ dùng vòng lặp foreach kết hợp với updateOrCreate(). Hậu quả là sinh ra 10.000 câu lệnh SQL bắn liên phanh vào Database, gây chết nghẽn hệ thống.
Một Senior sẽ đóng gói toàn bộ thành 1 câu SQL duy nhất bằng tính năng Upsert của Query Builder:
$data = [
['sku' => 'PROD-001', 'price' => 150000, 'stock' => 50],
['sku' => 'PROD-002', 'price' => 200000, 'stock' => 10],
// ... 4998 mảng nữa
];
DB::table('products')->upsert(
$data,
['sku'], // Cột Unique dùng để định danh (Biết sản phẩm nào trùng)
['price', 'stock'] // Nếu trùng SKU, thì chỉ cập nhật 2 cột Price và Stock
);
Chỉ với 1 cú hit (1 lệnh thực thi), toàn bộ 5000 bản ghi được xử lý xong trong vài mili-giây ở tầng lõi C của Database engine.
4. Vung dao găm DB::raw() và Lá chắn chống SQL Injection
Sức mạnh lớn nhất của Query Builder là nó cho phép bạn chèn các đoạn SQL "thô" (Raw SQL) để sử dụng các hàm đặc thù của Database (như COUNT(), DATE_FORMAT(), hàm xử lý JSON).
Tuy nhiên, DB::raw() là một con dao hai lưỡi. Nó sẽ chọc thủng hoàn toàn cơ chế bảo vệ PDO của Laravel, mở toang cánh cửa cho Hacker thực hiện SQL Injection nếu bạn nối chuỗi nối thẳng biến $request vào đó.
Sai lầm chết người:
// ❌ HACKER SẼ DROP TOÀN BỘ DATABASE VỚI DÒNG CODE NÀY
$station = $request->input('station_id');
DB::table('transactions')
->whereRaw("station_id = '" . $station . "'")
->get();
Cách dùng chuẩn Senior (Sử dụng Parameter Binding): Luôn luôn sử dụng dấu ? để PDO tự động "lọc độc" (escape) dữ liệu trước khi ghép vào câu SQL.
$station = $request->input('station_id');
// ✅ Tuyệt đối an toàn
DB::table('transactions')
->whereRaw('station_id = ?', [$station])
->get();
// Hoặc sử dụng các hàm Raw chuyên biệt đã được Laravel bọc an toàn:
DB::table('transactions')
->selectRaw('station_id, COUNT(*) as total_taps')
->groupBy('station_id')
->havingRaw('COUNT(*) > ?', [5000])
->get();
Thông qua Bài 1, chúng ta đã thiết lập lại tư duy cốt lõi về hiệu năng: Chọn đúng vũ khí cho đúng trận chiến. Eloquent dùng cho các tác vụ CRUD cần tính toàn vẹn Logic, còn Query Builder là cỗ máy ủi hạng nặng để xử lý, xuất nhập lượng dữ liệu khổng lồ với độ an toàn và tốc độ tối đa.
All rights reserved