Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
Web Development Essentials

Web Development Essentials 035.3: Khái niệm cơ bản về SQL

Web Development Essentials 035.3: Khái niệm cơ bản về SQL

Tạo bảng SQLite, hiểu khóa chính và CHECK, dùng CRUD có WHERE và bind tham số từ Node.js; kiểm tra transaction rollback, mở lại dữ liệu và API driver.

Danh sách yêu cầu đặt trong array mất đi khi tiến trình kết thúc. Chuyển sang cơ sở dữ liệu giúp lưu bền hơn, nhưng một câu UPDATE thiếu WHERE lại có thể sửa cả bảng. Một câu truy vấn ghép trực tiếp đầu vào còn trộn dữ liệu với cú pháp SQL. Bài này dùng cơ sở dữ liệu DEMO riêng để học lưu trữ, truy vấn và giới hạn thay đổi bằng điều kiện rõ ràng.

Bài 035.3, Khái niệm cơ bản về SQL, tham chiếu giáo trình Web Development Essentials 030-100 trang in 339–354. Nội dung độc lập được đối chiếu ngày 23/10/2026. Bạn cần hiểu Node, hàm, object, lỗi và thư mục làm việc. Sau bài học, bạn có thể tạo bảng, dùng khóa chính, thực hiện CRUD qua Node, bind tham số và xác nhận dữ liệu còn sau khi đóng/mở lại kết nối.

SQL và SQLite trong chương trình Node

SQL là ngôn ngữ để định nghĩa và thao tác dữ liệu trong các hệ quản trị hỗ trợ nó. SQLite là engine nhúng, không cần tiến trình database server riêng cho lab này. Chương trình mở một tệp và thực thi qua thư viện. Điều đó khác kết nối TCP tới PostgreSQL/MySQL, dù các câu SQL cơ bản có nhiều điểm tương đồng. Không suy từ một API SQLite sang API của mọi driver.

Tệp database không tự trở thành tài nguyên công khai. Đặt nó ngoài thư mục public/static của server và giới hạn quyền filesystem. Dữ liệu còn sau restart không thay thế backup hoặc chính sách lưu trữ. Khi dùng WAL, có thể có thêm tệp phụ liên quan; sao chép tùy tiện chỉ một tệp lúc database đang hoạt động không phải phương án backup tổng quát.

Đường dẫn tương đối thường được giải theo thư mục làm việc của tiến trình, không mặc nhiên theo vị trí script. Lab dùng URL tương đối với import.meta.url để xác định tệp cạnh script. Trong ứng dụng thật, chọn vị trí dữ liệu riêng có quyền ghi phù hợp; không chạy cả server bằng quyền quản trị chỉ để ghi được một tệp.

Cải biên driver và đọc API cũ

PDF dùng package sqlite3 với API callback. Repository gốc TryGhost/node-sqlite3 đã được chủ sở hữu archive ngày 01/07/2026 và đang read-only khi đối chiếu. Lab mới chọn better-sqlite3 13.0.3, bản registry công bố ngày 05/08/2026, dùng Node 24 LTS. Lựa chọn này cần được kiểm tra lại ở đợt xuất bản sau; không nhận các driver là thay thế trực tiếp từng dòng.

Trong sqlite3 cũ, Database.run thực thi thay đổi, Database.all trả array qua callback, Database.get trả hàng đầu và Database.each gọi callback theo từng hàng. Database.close đóng kết nối. LastID/changes gắn với this của callback function thích hợp, nên thay arrow function vào đó có thể làm sai ngữ nghĩa.

Với better-sqlite3, tạo Database, prepare câu SQL rồi dùng statement.run, get, all hoặc iterate; close vẫn là thao tác trên database. API này đồng bộ: công việc database dài có thể giữ luồng JavaScript chính. Lab CLI nhỏ phù hợp để hiểu cơ chế, không phải benchmark hay khẳng định cấu hình này tối ưu mọi tải web.

Native addon có thể cần binary phù hợp Node, kiến trúc và hệ điều hành, hoặc công cụ build. Nếu cài thất bại, đọc lỗi tương thích và hướng dẫn package trước khi đổi quyền toàn hệ thống. Không dùng ignore-scripts một cách máy móc khi package cần bước dựng native được kiểm soát. Môi trường kiểm chứng của bài được ghi riêng sau lab.

Bảng, khóa và ràng buộc

Một bảng có các cột và hàng. Primary key xác định bản ghi duy nhất theo thiết kế schema. Trong bảng SQLite rowid thông thường, cột khai báo đúng kiểu INTEGER PRIMARY KEY là bí danh của rowid và có thể được tự cấp số khi bỏ qua giá trị lúc insert. AUTOINCREMENT không cần chỉ để có số tự tăng; nó thay thuật toán để tránh dùng lại rowid đã từng cấp, kèm chi phí riêng.

Không coi rowid ngầm là cách thay thiết kế khóa rõ ràng cho mọi bảng. SQLite còn có WITHOUT ROWID và các quy tắc đặc thù. Cũng không dùng id tuần tự làm bí mật phân quyền: người khác có thể đoán số tiếp theo. Khi thao tác dữ liệu nhiều người, điều kiện sở hữu và kiểm tra quyền phải lấy từ danh tính đã xác thực, không phải một cookie tùy ý do client tự chọn.

SQLite có type affinity. Viết CHAR(36) không tự giới hạn đúng 36 ký tự như người đọc có thể suy từ một hệ SQL khác. Dùng ràng buộc rõ và validation ứng dụng. Lab dùng STRICT table cùng NOT NULL và CHECK cho một số yêu cầu nhỏ. Những kiểm tra này bảo vệ schema, vẫn cần thông báo lỗi dễ hiểu ở lớp ứng dụng.

Chuỗi literal SQL thường dùng nháy đơn; định danh có quy tắc trích dẫn riêng. Để dữ liệu động ra khỏi SQL text bằng parameter binding thay vì tự thêm nháy quanh nó. Tên bảng và cột cần chọn từ schema hoặc allowlist; placeholder cho giá trị không phải cách thay tên cột tùy ý.

Chuẩn bị cấu trúc SQL rồi bind giá trị riêng; kiểm tra điều kiện, kết quả và lỗi.
Hình 1. Chuẩn bị cấu trúc SQL rồi bind giá trị riêng; kiểm tra điều kiện, kết quả và lỗi. Mở hình để xem chi tiết.

CRUD, WHERE và thứ tự kết quả

Create ở mức dữ liệu thường là INSERT; đọc là SELECT; sửa là UPDATE và xóa là DELETE. CREATE TABLE định nghĩa cấu trúc. Chọn cột cần dùng thay vì mặc định SELECT mọi thứ. WHERE giới hạn hàng, ORDER BY xác định thứ tự và LIMIT giới hạn số kết quả khi thích hợp. Không có ORDER BY thì đừng dựa vào thứ tự nhìn thấy trong một lần chạy.

UPDATE/DELETE không có WHERE có thể tác động toàn bảng. Trước thao tác có ảnh hưởng, đọc cùng điều kiện bằng SELECT trên dữ liệu đúng phạm vi, rồi kiểm tra số changes. Lab chỉ dùng tệp DEMO tự tạo, không mở database của website. Trong production còn cần quyền, backup, transaction và quy trình thay đổi phù hợp.

Tham số bind giữ dữ liệu tách khỏi cấu trúc câu SQL đã chuẩn bị. Nó không chỉ là “xóa ký tự nguy hiểm”. Dấu nháy trong một tiêu đề hợp lệ vẫn được lưu đúng. Binding cũng không thay validation, authorization hoặc escape HTML lúc hiển thị. Một chuỗi được lưu an toàn trong SQLite vẫn cần xử lý đúng ngữ cảnh đầu ra EJS ở bài trước.

Lab: tệp DEMO riêng và câu truy vấn có tham số

Tạo thư mục mới. Lab tạo tickets-demo.sqlite cạnh script, chèn dữ liệu giả và có một bước xóa đúng bản ghi vừa tạo. Không đổi đường dẫn sang database thật. Cài driver pin phiên bản; lưu package.json cùng lockfile. Nếu hệ thống yêu cầu toolchain native, chuẩn bị theo tài liệu package trong môi trường lab riêng.

npm init --yes
npm pkg set type=module
npm pkg set private=true --json
npm install --save-exact --no-audit --no-fund better-sqlite3@13.0.3

Lưu lab.js. Database mới sẽ có bảng tickets; IF NOT EXISTS không biến schema cũ thành schema mới khi thay câu CREATE. Nếu chạy lại với một tệp đã có cấu trúc khác, dùng bản DEMO mới để kiểm tra thay vì nhận thông báo không lỗi là bằng chứng migration đã diễn ra.

import Database from "better-sqlite3";
import { fileURLToPath } from "node:url";
const filename = fileURLToPath(new URL("./tickets-demo.sqlite", import.meta.url));
let db = new Database(filename);
try {
  db.exec(`CREATE TABLE IF NOT EXISTS tickets (
    id INTEGER PRIMARY KEY,
    owner TEXT NOT NULL,
    subject TEXT NOT NULL CHECK(length(subject) BETWEEN 1 AND 80),
    status TEXT NOT NULL CHECK(status IN ('open', 'closed'))
  ) STRICT`);
  const insert = db.prepare("INSERT INTO tickets(owner, subject, status) VALUES (?, ?, ?)");
  const info = insert.run("DEMO-A", "Máy in O'Lan", "open");
  const id = info.lastInsertRowid;
  const find = db.prepare("SELECT id, subject, status FROM tickets WHERE id = ? AND owner = ?");
  console.log("INSERTED", find.get(id, "DEMO-A").subject);
  const update = db.prepare("UPDATE tickets SET status = ? WHERE id = ? AND owner = ?");
  console.log("WRONG-OWNER", update.run("closed", id, "DEMO-B").changes);
  console.log("UPDATED", update.run("closed", id, "DEMO-A").changes);
  const batch = db.transaction(() => {
    insert.run("DEMO-A", "Dòng thử sẽ rollback", "open");
    throw new Error("DEMO rollback");
  });
  try { batch(); } catch (error) { console.log("ROLLED-BACK", error.message); }
  console.log("ROLLBACK-ROWS", db.prepare("SELECT count(*) AS n FROM tickets WHERE subject = ?").get("Dòng thử sẽ rollback").n);
  db.close();
  db = new Database(filename);
  console.log("REOPENED", db.prepare("SELECT status FROM tickets WHERE id = ?").get(id).status);
  const remove = db.prepare("DELETE FROM tickets WHERE id = ? AND owner = ?");
  console.log("DELETED", remove.run(id, "DEMO-A").changes);
} finally {
  if (db.open) db.close();
}

Chạy node lab.js. Kỳ vọng INSERTED giữ nguyên dấu nháy trong “Máy in O'Lan”; WRONG-OWNER là 0, UPDATED là 1. Transaction thêm dòng rồi phát sinh lỗi nên ROLLBACK-ROWS bằng 0. Sau reopen, trạng thái vẫn closed. Cuối cùng DELETED là 1; script không xóa toàn bộ bảng hoặc tệp. Chủ sở hữu DEMO ở đây là điều kiện minh họa, chưa phải hệ authentication thật.

Thử SELECT bằng get khi không có id phù hợp để thấy undefined trước khi đọc thuộc tính. All trả array các hàng, còn iterate cho phép duyệt từng hàng theo API driver. Với dữ liệu ít, có thể dùng SELECT id, subject FROM tickets ORDER BY id để quan sát. Đừng sửa chương trình thành SELECT không điều kiện trên một bảng production chỉ để thử các phương thức.

Kiểm tra ràng buộc và tính nguyên vẹn

Trong một tệp DEMO khác hoặc transaction thử riêng, bind chuỗi có dấu nháy vào subject và truy vấn lại bằng tham số. Chuỗi phải được coi là dữ liệu. Thử status không thuộc open/closed phải bị CHECK từ chối; subject rỗng cũng không được nhận. Giữ error handler có thể phân biệt lỗi constraint với lỗi mở tệp hoặc lỗi cú pháp, thay vì trả thành công cho mọi exception.

Transaction gom các thay đổi thành một đơn vị theo quy tắc của database. Lab cố tình throw để xác nhận rollback, không mô phỏng mất điện hoặc lỗi ổ đĩa. Backup/restore và độ bền khi sự cố là bài toán khác. Việc close rồi reopen kiểm tra dữ liệu đã lưu ở đường chạy bình thường, không chứng minh mọi failure mode đã được xử lý.

Fixture xác nhận scoped CRUD, rollback, reopen và ràng buộc, trong phạm vi tệp DEMO.
Hình 2. Fixture xác nhận scoped CRUD, rollback, reopen và ràng buộc, trong phạm vi tệp DEMO. Mở hình để xem chi tiết.

Lab đã chạy ngày 23/10/2026 với Node 24.22.0, better-sqlite3 13.0.3 và SQLite 3.53.4 trong container user không phải root, không mạng ngoài, chỉ thư mục tạm có quyền ghi. Assertion xác nhận toàn bộ log, rollback, reopen, scoped changes, CHECK, bind chuỗi có dấu nháy, get không khớp, all/iterate và integrity_check ok. Thư mục tạm được xóa sau kiểm tra. Chưa thử HTTP, authentication, nhiều kết nối, backup/restore hoặc mất điện.

Kết thúc và tự kiểm tra

Đóng kết nối sau khi hoàn tất công việc. Với server thật, dừng nhận request mới và chờ các tác vụ đang xử lý theo thiết kế trước khi đóng database, thay vì đóng database trong khi handler vẫn cần nó. Không nhận việc SIGINT không báo lỗi là kiểm chứng recovery sau crash. Xóa tệp DEMO chỉ khi đã xác nhận không còn cần bằng chứng lab và đường dẫn đúng.

Vì sao bind tham số vẫn giữ được dấu nháy hợp lệ? AUTOINCREMENT khác INTEGER PRIMARY KEY ở điểm nào? Owner từ client có thể thay xác thực không? Vì sao CHECK và STRICT chưa thay validation ứng dụng? Nếu all cho kết quả đúng một lần, có được dựa vào thứ tự khi thiếu ORDER BY không? Trả lời được các câu này giúp bạn nối phần lưu dữ liệu vào ứng dụng mà vẫn nhận ra ranh giới của lab nhập môn.

Nguồn tham khảo

Bài liên quan trong bộ LPI

Tìm hiểu thêm