Kết nối MySQL với PHP bằng PDO: làm CRUD an toàn trước SQL Injection

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Kết nối cơ sở dữ liệu là bước đầu tiên của hầu hết ứng dụng PHP. Bài này hướng dẫn dùng PDO để làm đủ bốn thao tác thêm, đọc, sửa, xóa với MySQL, đặc biệt nhấn mạnh prepared statement để tránh lỗ hổng SQL Injection ngay từ đầu.

Vì sao dùng PDO

PDO (PHP Data Objects) là lớp truy cập dữ liệu thống nhất của PHP, hỗ trợ nhiều hệ cơ sở dữ liệu. Nó cung cấp prepared statement, xử lý lỗi bằng ngoại lệ và API nhất quán. Các hàm mysql_* cũ đã bị loại bỏ, còn mysqli là lựa chọn khác nhưng chỉ dành cho MySQL.

Bước 1: Tạo cơ sở dữ liệu và bảng

Dùng phpMyAdmin hoặc dòng lệnh MySQL chạy đoạn SQL sau:

CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE demo;

CREATE TABLE students (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(150) NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

Bảng mã utf8mb4 lưu đúng tiếng Việt có dấu và cả ký tự biểu tượng.

Bước 2: Kết nối bằng PDO

Tạo file db.php dùng chung cho cả dự án:

<?php
$host = '127.0.0.1';
$db   = 'demo';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    exit('Không kết nối được cơ sở dữ liệu.');
}

Ba tùy chọn quan trọng: chế độ ngoại lệ để lỗi không bị bỏ qua âm thầm, kiểu lấy dữ liệu mặc định là mảng kết hợp, và tắt giả lập prepared statement để dùng bản thật của MySQL. Ghi lỗi chi tiết vào log chứ đừng in ra cho người dùng, vì thông báo lỗi có thể lộ thông tin hệ thống. Trong dự án thật, thông tin đăng nhập nên nằm trong biến môi trường hoặc file cấu hình ngoài mã nguồn công khai, và không dùng tài khoản root.

SQL Injection là gì

Nếu bạn ghép trực tiếp dữ liệu người dùng vào câu lệnh SQL, kẻ xấu có thể chèn thêm lệnh. Ví dụ đoạn code nguy hiểm sau:

// KHÔNG LÀM NHƯ THẾ NÀY
$sql = "SELECT * FROM students WHERE email = '" . $_GET['email'] . "'";

Chỉ cần gửi giá trị đặc biệt là câu lệnh có thể bị thay đổi ý nghĩa. Giải pháp là prepared statement: câu SQL và dữ liệu được gửi tách biệt, dữ liệu không bao giờ được diễn giải như lệnh.

Bước 3: CRUD với prepared statement

<?php
require 'db.php';

// CREATE
$stmt = $pdo->prepare('INSERT INTO students (name, email) VALUES (:name, :email)');
$stmt->execute(['name' => 'Nguyễn An', 'email' => 'an@example.com']);
$newId = $pdo->lastInsertId();

// READ - danh sách
$stmt = $pdo->query('SELECT id, name, email FROM students ORDER BY id DESC');
$students = $stmt->fetchAll();

// READ - một bản ghi theo id
$stmt = $pdo->prepare('SELECT * FROM students WHERE id = :id');
$stmt->execute(['id' => $newId]);
$student = $stmt->fetch();

// UPDATE
$stmt = $pdo->prepare('UPDATE students SET name = :name WHERE id = :id');
$stmt->execute(['name' => 'Nguyễn Văn An', 'id' => $newId]);
echo 'Số dòng đã sửa: ' . $stmt->rowCount();

// DELETE
$stmt = $pdo->prepare('DELETE FROM students WHERE id = :id');
$stmt->execute(['id' => $newId]);

Dấu hai chấm như :name là tham số có tên. Với truy vấn không có dữ liệu người dùng như danh sách đơn giản, query() là đủ. Khi có bất kỳ giá trị nào đến từ bên ngoài, hãy luôn dùng prepare và execute.

Bước 4: Hiển thị an toàn ra HTML

Chống SQL Injection chưa đủ, bạn còn phải chống XSS khi in dữ liệu ra trang. Hãy bọc đầu ra bằng htmlspecialchars:

<?php foreach ($students as $s): ?>
  <tr>
    <td><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></td>
    <td><?= htmlspecialchars($s['email'], ENT_QUOTES, 'UTF-8') ?></td>
  </tr>
<?php endforeach; ?>

Transaction khi cần nhiều bước

Khi một nghiệp vụ gồm nhiều câu lệnh phải cùng thành công hoặc cùng thất bại, dùng giao dịch:

try {
    $pdo->beginTransaction();
    // ... nhiều câu lệnh prepare/execute ...
    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

Những lỗi cần tránh

  • Ghép chuỗi SQL với dữ liệu từ người dùng.
  • Dùng tài khoản root cho ứng dụng thật, nên tạo tài khoản với quyền tối thiểu.
  • In thông báo lỗi PDO ra màn hình ở môi trường chạy thật.
  • Quên đặt charset khiến tiếng Việt bị lỗi hiển thị.
  • Lưu mật khẩu dạng văn bản thô; hãy dùng password_hash.

Kết luận

Với PDO và prepared statement, bạn vừa có code CRUD gọn vừa có lớp phòng thủ đầu tiên trước SQL Injection. Nhiều framework như Laravel dựa trên cùng nguyên lý này, nên hiểu rõ nền tảng sẽ giúp bạn dùng framework tự tin hơn.

Để học PHP và xây dựng ứng dụng web hoàn chỉnh, bạn có thể xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment