Bài 3. Thêm dữ liệu và câu lệnh Prepare

Bài này nối form HTML với CSDL: nhận dữ liệu sinh viên từ form, kiểm tra hợp lệ và thêm vào bảng bằng prepared statement. Đây là kỹ thuật quan trọng nhất của loạt bài vì nó ngăn chặn tận gốc SQL Injection.

50 phútTrung cấp
Bạn sẽ học được
  • Hiểu bốn bước prepare → bind_param → execute → close
  • Thêm bản ghi từ dữ liệu form an toàn
  • Lấy id vừa thêm, xử lý lỗi trùng khóa
  • Thêm nhiều bản ghi với một statement, dùng transaction
Thuật ngữ cần biết
Placeholder
Dấu ? trong câu SQL giữ chỗ cho giá trị.
bind_param
Gắn biến PHP vào placeholder kèm kiểu: s, i, d, b.
insert_id
id tự tăng của bản ghi vừa thêm.
Duplicate entry
Lỗi trùng khóa UNIQUE/PRIMARY (mã 1062).

Khái niệm và lý thuyết

Thay vì ghép giá trị vào chuỗi SQL, ta gửi mẫu có dấu ? rồi gửi giá trị riêng. MySQL biên dịch mẫu một lần và coi giá trị là dữ liệu thuần, nên chuỗi như '; DROP TABLE sinhvien; -- chỉ là một cái tên kỳ lạ, không phải lệnh. Xem loạt SQL, Bài 10 để hiểu cơ chế tấn công.

Ký tự kiểuKiểu PHP
sstring (chuỗi, ngày dạng chuỗi)
iinteger
ddouble/float
bblob (nhị phân)

Form thêm sinh viên

<?php
// them.php
require_once __DIR__ . "/db_connect.php";
function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
$loi = []; $d = ["ma_sv" => "", "ho_ten" => "", "lop" => "", "ngay_sinh" => "", "email" => ""];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    foreach ($d as $k => $_) $d[$k] = trim($_POST[$k] ?? "");
    if (!preg_match('/^SV\d{3,8}$/', $d["ma_sv"])) $loi["ma_sv"] = "Mã dạng SV + số";
    if (mb_strlen($d["ho_ten"]) < 3) $loi["ho_ten"] = "Họ tên quá ngắn";
    if ($d["email"] !== "" && !filter_var($d["email"], FILTER_VALIDATE_EMAIL)) $loi["email"] = "Email sai";
    $ns = $d["ngay_sinh"] !== "" ? $d["ngay_sinh"] : null;      // input type=date gửi Y-m-d

    if (!$loi) {
        $sql = "INSERT INTO sinhvien (ma_sv, ho_ten, lop, ngay_sinh, email) VALUES (?, ?, ?, ?, ?)";
        $stmt = $conn->prepare($sql);
        $stmt->bind_param("sssss", $d["ma_sv"], $d["ho_ten"], $d["lop"], $ns, $d["email"]);
        try {
            $stmt->execute();
            header("Location: danh-sach.php?them=" . $stmt->insert_id, true, 303);
            exit;
        } catch (mysqli_sql_exception $ex) {
            $loi["chung"] = $ex->getCode() === 1062 ? "Mã sinh viên đã tồn tại" : "Không lưu được dữ liệu";
        } finally {
            $stmt->close();
        }
    }
}
?>
<form method="post">
  <p style="color:red"><?= e($loi["chung"] ?? "") ?></p>
  <p>Mã SV <input name="ma_sv" value="<?= e($d["ma_sv"]) ?>" required> <small><?= e($loi["ma_sv"] ?? "") ?></small></p>
  <p>Họ tên <input name="ho_ten" value="<?= e($d["ho_ten"]) ?>" required> <small><?= e($loi["ho_ten"] ?? "") ?></small></p>
  <p>Lớp <input name="lop" value="<?= e($d["lop"]) ?>"></p>
  <p>Ngày sinh <input type="date" name="ngay_sinh" value="<?= e($d["ngay_sinh"]) ?>"></p>
  <p>Email <input type="email" name="email" value="<?= e($d["email"]) ?>"> <small><?= e($loi["email"] ?? "") ?></small></p>
  <button>Thêm sinh viên</button>
</form>

Thêm nhiều bản ghi và transaction

<?php
$ds = [["SV101", "Lò Văn Khánh", "K66 Tin"], ["SV102", "Cầm Thị Liên", "K66 Tin"]];
$stmt = $conn->prepare("INSERT INTO sinhvien (ma_sv, ho_ten, lop) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $ma, $ten, $lop);     // gắn biến một lần
$conn->begin_transaction();
try {
    foreach ($ds as [$ma, $ten, $lop]) $stmt->execute();   // đổi giá trị biến rồi execute lại
    $conn->commit();
    echo "Đã thêm " . count($ds) . " sinh viên";
} catch (mysqli_sql_exception $e) {
    $conn->rollback();                       // một dòng lỗi: hủy tất cả
    echo "Lỗi, không thêm dòng nào";
}

Mẫu này dùng để nhập điểm cả lớp hoặc nhập sinh viên từ tệp CSV (kết hợp fgetcsv ở loạt PHP, Bài 13).

Cùng việc bằng PDO

<?php
$st = $pdo->prepare("INSERT INTO sinhvien (ma_sv, ho_ten, lop) VALUES (:ma, :ten, :lop)");
$st->execute([":ma" => $ma, ":ten" => $ten, ":lop" => $lop]);
echo $pdo->lastInsertId();
Mẹo hay

Sau khi thêm thành công, chuyển hướng (PRG) sang trang danh sách kèm thông báo; người dùng bấm F5 sẽ không thêm trùng bản ghi.

Lưu ý

Không đặt nháy quanh dấu ?: VALUES ('?') là chuỗi chứa dấu hỏi chứ không phải placeholder, lỗi rất hay gặp của người mới.

Thực hành
  1. Hoàn thiện them.php với đầy đủ kiểm tra lỗi.
  2. Thử nhập tên chứa dấu nháy (O'Brien) và chuỗi tấn công, kiểm tra dữ liệu lưu đúng.
  3. Viết trang nhập điểm: chọn môn, nhập điểm cho 5 sinh viên, lưu bằng một transaction.
  4. Nhập danh sách sinh viên từ tệp CSV vào CSDL.
Tóm tắt bài học
  • prepare → bind_param → execute → close.
  • Kiểu s, i, d, b; không đặt nháy quanh ?.
  • insert_id lấy id mới; mã lỗi 1062 là trùng khóa.
  • Transaction + một statement cho nhiều bản ghi.