Hướng dẫn chạy SQL Raw (SQL Thuần) trong Laravel
Trong Laravel, bên cạnh Eloquent ORM, bạn hoàn toàn có thể viết và thực thi trực tiếp các câu lệnh SQL thuần (Raw SQL). Điều này rất hữu ích khi bạn muốn toàn quyền kiểm soát câu lệnh SQL, cần tối ưu hóa tốc độ tối đa hoặc xử lý các truy vấn phức tạp (thống kê, gom nhóm, JOIN nhiều bảng).
1. Khởi đầu: Facade DB
Mọi thao tác với SQL thuần trong Laravel đều đi qua Facade Illuminate\Support\Facades\DB.
use Illuminate\Support\Facades\DB;
Quy ước dự án: Các truy vấn DB nên được viết bên trong tầng Service (
app/Services/*) hoặc Repository, tránh viết trực tiếp tràn lan trong Controller hay View Blade.
2. Các hàm thực thi SQL thuần cơ bản
Laravel phân chia các hàm tương ứng với từng loại hành động trong SQL:
| Hành động | Hàm của Laravel | Kiểu kết quả trả về |
|---|---|---|
| Đọc dữ liệu | DB::select(...) |
array chứa các đối tượng stdClass |
| Thêm mới | DB::insert(...) |
bool (true/false) |
| Cập nhật | DB::update(...) |
int (số dòng bị tác động) |
| Xóa dữ liệu | DB::delete(...) |
int (số dòng đã xóa) |
| Lệnh tổng quát | DB::statement(...) |
bool |
① Đọc dữ liệu: DB::select()
DB::select() luôn trả về một mảng (array) các đối tượng stdClass. Bạn truy cập các cột như thuộc tính object thông thường: $item->tên_cột.
Ví dụ 1: Lấy danh sách bài viết Forum kèm thông tin tác giả (JOIN)
$posts = DB::select("
SELECT
p.id,
p.title,
p.likes_count,
p.comments_count,
p.created_at,
u.name AS author_name,
u.avatar_url
FROM forum_posts p
INNER JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
", [10, 0]);
// Duyệt mảng kết quả:
foreach ($posts as $post) {
echo $post->title;
echo $post->author_name;
}
Ví dụ 2: Lấy 1 bản ghi duy nhất
$post = DB::select("
SELECT * FROM forum_posts WHERE id = ? LIMIT 1
", [$postId]);
if (!empty($post)) {
$firstPost = $post[0];
echo $firstPost->title;
}
② Thêm mới dữ liệu: DB::insert()
Thêm dữ liệu bằng câu lệnh INSERT INTO.
$success = DB::insert("
INSERT INTO forum_posts (user_id, title, content, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
", [$userId, $title, $content]);
// Lấy ID tự tăng của bản ghi vừa chèn (lastInsertId):
$newPostId = DB::getPdo()->lastInsertId();
③ Cập nhật dữ liệu: DB::update()
Hàm trả về số dòng bị ảnh hưởng bởi câu lệnh UPDATE.
// Tăng số lượt like cho bài viết
$affectedRows = DB::update("
UPDATE forum_posts
SET likes_count = likes_count + 1, updated_at = NOW()
WHERE id = ?
", [$postId]);
if ($affectedRows > 0) {
// Đã cập nhật thành công
}
④ Xóa dữ liệu: DB::delete()
Hàm trả về số dòng đã được xóa.
// Xóa reaction like/dislike của một user
$deletedRows = DB::delete("
DELETE FROM forum_reactions
WHERE forum_post_id = ? AND user_id = ?
", [$postId, $userId]);
⑤ Chạy câu lệnh nâng cao: DB::statement()
Dùng cho các lệnh đặc biệt như DDL (ALTER, DROP) hoặc câu lệnh INSERT ... ON DUPLICATE KEY UPDATE của MySQL:
// Xử lý Toggle Like/Dislike trong 1 câu SQL duy nhất
DB::statement("
INSERT INTO forum_reactions (forum_post_id, user_id, type, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE
type = VALUES(type),
updated_at = NOW()
", [$postId, $userId, 'like']);
3. Bảo mật: Chống lỗi SQL Injection (BẮT BUỘC)
[!CAUTION] Tuyệt đối KHÔNG nối chuỗi biến trực tiếp vào câu lệnh SQL! Kẻ xấu có thể nhập chuỗi độc hại như
' OR '1'='1để phá hoại cơ sở dữ liệu.
❌ Cách viết NGUY HIỂM (Không bao giờ dùng):
// RẤT NGUY HIỂM: Bị SQL Injection
$id = $request->input('id');
$posts = DB::select("SELECT * FROM forum_posts WHERE id = " . $id);
✅ Cách viết AN TOÀN (Parameter Binding):
Laravel hỗ trợ 2 kiểu binding tham số an toàn:
Cách 1: Dùng dấu hỏi chấm ? (Positional Binding - khuyên dùng)
$posts = DB::select("
SELECT * FROM forum_posts
WHERE user_id = ? AND likes_count >= ?
", [$userId, 10]);
Cách 2: Dùng tên nhãn :tên (Named Binding)
$posts = DB::select("
SELECT * FROM forum_posts
WHERE user_id = :userId AND likes_count >= :minLikes
", [
'userId' => $userId,
'minLikes' => 10,
]);
4. Kết hợp SQL Raw vào Eloquent Query Builder
Nếu bạn vẫn muốn dùng Eloquent để tận dụng Model, nhưng cần chèn một vài biểu thức SQL đặc thù, hãy dùng các hàm Raw có sẵn:
① selectRaw(): Tính toán cột phức tạp
use App\Models\ForumPost;
$stats = ForumPost::selectRaw('user_id, COUNT(*) as total_posts, SUM(likes_count) as total_likes')
->groupBy('user_id')
->havingRaw('total_posts > ?', [5])
->get();
② whereRaw(): Điều kiện lọc đặc biệt
// Lọc các bài viết được tạo trong vòng 7 ngày qua
$recentPosts = ForumPost::whereRaw('created_at >= NOW() - INTERVAL 7 DAY')
->latest()
->get();
③ orderByRaw(): Sắp xếp theo công thức tùy chỉnh
// Sắp xếp bài viết nổi bật dựa trên điểm: (like * 2) - dislike
$trendingPosts = ForumPost::orderByRaw('(likes_count * 2) - dislikes_count DESC')
->take(10)
->get();
5. Quản lý Giao dịch (Database Transaction)
Khi thực hiện nhiều câu lệnh SQL phụ thuộc lẫn nhau (ví dụ: trừ tiền tài khoản A và cộng tiền tài khoản B, hoặc tạo bài viết và chèn nhiều ảnh), nếu một câu lệnh lỗi, toàn bộ phải được hoàn tác (Rollback).
Cách 1: Tự động đóng/mở Transaction (Closure)
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($userId, $title, $content, $images) {
// 1. Chèn bài viết
DB::insert("
INSERT INTO forum_posts (user_id, title, content, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
", [$userId, $title, $content]);
$postId = DB::getPdo()->lastInsertId();
// 2. Chèn danh sách ảnh
foreach ($images as $order => $path) {
DB::insert("
INSERT INTO forum_post_images (forum_post_id, image_path, display_order, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
", [$postId, $path, $order]);
}
});
// Nếu có bất kỳ Exception nào xảy ra, Laravel tự động Rollback lại sạch sẽ.
Cách 2: Điều khiển thủ công (Manual Transaction)
DB::beginTransaction();
try {
DB::insert(...);
DB::update(...);
DB::commit(); // Xác nhận lưu vĩnh viễn
} catch (\Throwable $e) {
DB::rollBack(); // Hoàn tác lại như chưa có gì xảy ra
throw $e;
}
6. So sánh tổng kết: Khi nào dùng gì?
| Tiêu chí | Eloquent ORM | SQL Raw (DB::select) |
|---|---|---|
| Tốc độ code | Cực nhanh, cú pháp ngắn gọn, dễ bảo trì | Phải viết câu lệnh SQL thủ công |
| Hiệu năng xử lý | Đủ nhanh cho 95% trường hợp ứng dụng web | Tối đa (bỏ qua bước hydrate đối tượng Model) |
| Tính năng kèm theo | Quan hệ (hasMany, belongsTo), Casts, Events, Mutators |
Chỉ trả về dữ liệu thô (stdClass) |
| Phù hợp nhất cho | Các tính năng CRUD thường ngày (bài viết, user, bình luận) | Báo cáo thống kê, tác vụ nền xử lý triệu dòng, query siêu phức tạp |
[!TIP] Xem câu lệnh SQL thực tế đang chạy: Bạn có thể bật
DB::listentrongAppServiceProvider.phphoặc sử dụngtoSql()trên Query Builder để xem câu lệnh MySQL chính xác mà Laravel vừa chạy:\Log::info(ForumPost::where('id', 1)->toSql());