EzCode # Bài 7 — Thiết kế cơ sở dữ liệu EzCode
Học xong bài này bạn sẽ:
- Nạp được toàn bộ dữ liệu EzCode vào MySQL bằng phpMyAdmin
- Đọc hiểu câu lệnh
CREATE TABLEvà ý nghĩa từng kiểu cột- Hiểu quan hệ một-nhiều và nhiều-nhiều giữa 6 bảng của project
1. Vấn đề đặt ra
Ở Bài 3, bạn tự khai báo bằng tay một mảng ba khóa học rồi in ra bằng foreach. Nó chạy được.
Nhưng thử nghĩ xem chuyện gì xảy ra khi:
- Giảng viên muốn thêm khóa học thứ 15 — bạn phải sửa file PHP.
- Học viên mua một khóa — bạn phải sửa file PHP để ghi nhận.
- Có 1000 học viên cùng mua một lúc — file PHP nào chịu nổi.
- Máy chủ khởi động lại — mọi biến trong bộ nhớ biến mất.
Dữ liệu cần một nơi lưu trữ lâu dài, có tổ chức, và nhiều người dùng được cùng lúc. Đó là cơ sở dữ liệu.
Bài này không dạy bạn viết ứng dụng. Nó dạy bạn đọc hiểu kho dữ liệu mà EzCode đang dùng, để 15 bài sau bạn biết mình đang lấy dữ liệu từ đâu và vì sao câu truy vấn lại viết như vậy.
2. Kiến thức mới
2.1. Database, bảng, dòng, cột
Bốn khái niệm xếp lồng nhau:
- Database (cơ sở dữ liệu) — cả kho. EzCode có một database tên
ezcode. - Bảng (table) — một ngăn trong kho, chứa một loại dữ liệu. EzCode có 6 bảng.
- Cột (column) — một thuộc tính. Bảng
coursescó cộttitle,price,image... - Dòng (row) — một bản ghi cụ thể. Một dòng của
courseslà một khóa học.
Nếu bạn đã dùng Excel thì hình dung rất dễ: database là file Excel, bảng là một sheet, cột là cột, dòng là dòng.
Điều quan trọng nhất cần nối lại với Bài 3:
Một dòng trong bảng = một mảng kết hợp trong PHP. Tên cột trở thành khóa của mảng.
Dòng đầu tiên của bảng
courseskhi về tới PHP sẽ là:[ 'id' => 1, 'teacher_id' => 1, 'category_id' => 1, 'title' => 'Khóa học HTML CSS cơ bản', 'description' => 'Học cách xây dựng giao diện web từ đầu.', 'price' => 300000, 'image' => 'kh-lap-trinh-web-html-css.webp', 'status' => 1 ]Và nhiều dòng = mảng lồng nhau, đúng cấu trúc mà
foreachở Bài 3 duyệt qua.Bây giờ bạn hiểu vì sao code EzCode viết
$course['title']:titlelà tên cột trong bảngcourses.
2.2. Các kiểu cột dùng trong EzCode
Khi tạo cột, bạn phải khai báo nó chứa loại dữ liệu gì. MySQL cần biết để cấp phát bộ nhớ và kiểm tra tính hợp lệ.
| Kiểu | Chứa gì | Dùng ở đâu trong EzCode |
|---|---|---|
int(11) |
Số nguyên | id, price, teacher_id, sort_order |
tinyint(4) |
Số nguyên rất nhỏ, thường dùng như đúng/sai | courses.status |
varchar(225) |
Chuỗi ngắn, tối đa 225 ký tự | title, name, email, password |
text |
Chuỗi dài, tới 65.535 ký tự | description, lessions.content |
decimal(10,2) |
Số thập phân chính xác | payments.amount |
datetime |
Ngày và giờ | enrollments.enrolled_at |
timestamp |
Ngày giờ, tự cập nhật được | payments.created_at |
enum(...) |
Chỉ nhận một trong các giá trị liệt kê sẵn | users.role, payments.status |
Vài điểm đáng chú ý:
Con số trong ngoặc của int(11) không giới hạn giá trị. Nhiều người mới tưởng int(11) là "tối đa 11 chữ số". Sai. int luôn chứa được tới khoảng 2,1 tỷ bất kể số trong ngoặc. Con số đó chỉ là gợi ý về độ rộng khi hiển thị, và MySQL 8 đã bỏ hẳn nó.
varchar(225) là một con số lạ. Chuẩn thường dùng là 255 — giá trị lớn nhất mà MySQL lưu độ dài chuỗi trong 1 byte. Tác giả EzCode gõ 225 thay vì 255, gần như chắc chắn là gõ nhầm. Không gây lỗi gì, chỉ hơi lạ. Bạn cứ giữ nguyên để khớp với file ezcode.sql.
text khác varchar ở chỗ nào? varchar lưu cùng chỗ với dòng, đọc nhanh, nhưng giới hạn độ dài. text lưu ở chỗ khác, chứa được rất dài, nhưng không đánh chỉ mục toàn phần được. Quy tắc: nội dung ngắn có thể cần tìm kiếm thì dùng varchar; nội dung dài chỉ để đọc thì dùng text.
Vì sao payments.amount dùng decimal mà courses.price dùng int? decimal(10,2) lưu chính xác tuyệt đối, không bao giờ sai số — đúng chuẩn cho tiền tệ. int cũng chính xác nhưng không lưu được phần lẻ. Với đồng Việt Nam thì không cần phần lẻ, nên cả hai đều dùng được. Đây là sự thiếu nhất quán nhỏ trong thiết kế, không phải lỗi.
2.3. NULL và NOT NULL
NULL nghĩa là chưa có giá trị — khác hẳn với số 0 hay chuỗi rỗng.
NOT NULL— cột này bắt buộc phải có giá trị.DEFAULT NULL— cột này được phép để trống.DEFAULT 'student'— nếu không điền thì tự lấy giá trịstudent.
Nhìn bảng users, bạn sẽ thấy name, email, password, phone đều là DEFAULT NULL. Nghĩa là ở tầng database, bạn có thể tạo một người dùng không có email.
Điều đó nghe rất sai. Người dùng không email thì đăng nhập kiểu gì?
Câu trả lời: EzCode chặn việc đó ở tầng PHP, trong UserController::register() bằng đoạn if (empty($email)) $errors[] = ... mà bạn đã đọc ở Bài 4. Database không chặn, PHP chặn.
Đây là một lựa chọn thiết kế phổ biến nhưng kém an toàn: nếu có ai đó thêm dữ liệu bằng con đường khác — chẳng hạn gõ SQL thẳng trong phpMyAdmin — thì không gì ngăn được. Ràng buộc quan trọng nên đặt ở cả hai tầng.
2.4. Khóa chính và AUTO_INCREMENT
Khóa chính (primary key) là cột dùng để phân biệt từng dòng. Không dòng nào được trùng khóa chính, và khóa chính không được NULL.
ALTER TABLE `courses`
ADD PRIMARY KEY (`id`);
AUTO_INCREMENT bảo MySQL tự đánh số tăng dần:
ALTER TABLE `courses`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=22;
Nghĩa là: cột id tự tăng, và dòng tiếp theo sẽ nhận số 22.
Nhờ vậy khi thêm khóa học mới, PHP không phải nghĩ xem id nên là bao nhiêu. Nó chèn dữ liệu và MySQL tự cấp số. Sau đó PHP hỏi lại số vừa cấp bằng lastInsertId() — hàm này Bài 8 sẽ dạy.
Trong file ezcode.sql, các câu ALTER TABLE nằm ở cuối file chứ không nằm trong CREATE TABLE. Đây là cách phpMyAdmin xuất dữ liệu ra: tạo bảng trước, chèn dữ liệu, rồi mới thêm khóa và chỉ mục. Làm vậy nhanh hơn vì MySQL không phải cập nhật chỉ mục sau mỗi dòng chèn vào.
2.5. Khóa ngoại — nối hai bảng lại
Bảng courses có cột teacher_id chứa số. Số đó trỏ tới cột id của bảng users.
ALTER TABLE `courses`
ADD CONSTRAINT `courses_ibfk_2` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`);
Câu này bảo MySQL: "giá trị trong courses.teacher_id bắt buộc phải tồn tại trong users.id".
Tác dụng:
- Không tạo được khóa học với giảng viên không tồn tại. Chèn
teacher_id = 999khi bảnguserschỉ có tới id 35 sẽ bị từ chối. - Không xóa được giảng viên đang có khóa học. MySQL chặn để tránh bỏ lại khóa học mồ côi.
Điểm số 2 sẽ gây bất ngờ cho bạn ở Bài 18: bạn sẽ thấy một số khóa học không xóa được, vì chúng đang được tham chiếu từ bảng enrollments.
ON DELETE CASCADE thay đổi hành vi đó:
ALTER TABLE `payments`
ADD CONSTRAINT `payments_ibfk_1` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE;
CASCADE nghĩa là "xóa lan truyền": xóa một khóa học thì mọi dòng thanh toán của khóa đó tự động bị xóa theo.
Hãy để ý sự thiếu nhất quán trong EzCode: bảng payments có CASCADE, còn enrollments thì không. Nghĩa là xóa một khóa học sẽ xóa sạch lịch sử thanh toán nhưng lại bị chặn bởi bảng ghi danh. Đây là điểm thiết kế đáng bàn mà Bài 18 sẽ gặp lại.
2.6. Quan hệ một-nhiều
Một khóa học có nhiều bài học. Một bài học chỉ thuộc về một khóa. Đó là quan hệ một-nhiều.
Cách thể hiện: bảng ở phía "nhiều" giữ khóa ngoại.
courses lessions
┌────┬──────────────┐ ┌────┬───────────┬──────────────────┐
│ id │ title │ │ id │ course_id │ title │
├────┼──────────────┤ ├────┼───────────┼──────────────────┤
│ 1 │ HTML CSS │◄───────┤ 1 │ 1 │ Giới thiệu HTML │
│ │ │◄───────┤ 2 │ 1 │ CSS cơ bản │
│ 2 │ Flutter │◄───────┤ 3 │ 2 │ Cài đặt Flutter │
│ │ │◄───────┤ 4 │ 2 │ Widget cơ bản │
└────┴──────────────┘ └────┴───────────┴──────────────────┘
Bảng lessions giữ course_id. Muốn lấy mọi bài học của khóa số 1, bạn hỏi: "cho tôi mọi dòng trong lessions có course_id = 1".
Ba quan hệ một-nhiều trong EzCode:
| Bảng "một" | Bảng "nhiều" | Khóa ngoại |
|---|---|---|
courses |
lessions |
lessions.course_id |
users (giảng viên) |
courses |
courses.teacher_id |
categories |
courses |
courses.category_id |
2.7. Quan hệ nhiều-nhiều và bảng trung gian
Bây giờ tới quan hệ khó hơn: học viên và khóa học.
- Một học viên mua nhiều khóa học.
- Một khóa học được nhiều học viên mua.
Cả hai phía đều "nhiều". Không bảng nào giữ khóa ngoại được — bạn không thể nhét 500 học viên vào một cột của bảng courses.
Giải pháp là tạo bảng thứ ba ở giữa. Trong EzCode nó tên là enrollments (ghi danh):
users enrollments courses
┌────┬─────────┐ ┌─────────┬───────────┐ ┌────┬──────────┐
│ id │ name │ │ user_id │ course_id │ │ id │ title │
├────┼─────────┤ ├─────────┼───────────┤ ├────┼──────────┤
│ 2 │ Trần B │◄────┤ 2 │ 1 ├─────►│ 1 │ HTML CSS │
│ 3 │ Phạm C │◄────┤ 3 │ 2 ├─────►│ 2 │ Flutter │
└────┴─────────┘ └─────────┴───────────┘ └────┴──────────┘
Mỗi dòng trong enrollments nghĩa là "người này đã mua khóa kia".
Bảng trung gian còn giữ được thông tin về chính mối quan hệ đó, thứ không thuộc về bên nào cả:
price— giá tại thời điểm mua. Rất quan trọng: giảng viên tăng giá khóa học sau này thì lịch sử vẫn ghi đúng số tiền học viên đã trả.enrolled_at— mua lúc nào.role— vai trò trong khóa học đó.
2.8. Khóa chính ghép
Bảng enrollments không có cột id riêng. Thay vào đó:
ALTER TABLE `enrollments`
ADD PRIMARY KEY (`user_id`,`course_id`);
Khóa chính gồm hai cột cùng lúc. Nghĩa là cặp (user_id, course_id) không được trùng.
Hệ quả trực tiếp: một học viên không thể đăng ký cùng một khóa hai lần. Database tự chặn, không cần PHP kiểm tra.
Đây là một thiết kế tốt. Nhưng nó cũng có mặt trái mà bạn sẽ thấy ở Bài 22: vì không có cột id, bạn không thể xóa hay tham chiếu tới một dòng ghi danh cụ thể bằng một con số duy nhất.
2.9. enum — danh sách giá trị cố định
`role` enum('teacher','student') NOT NULL DEFAULT 'student'
Cột này chỉ nhận đúng hai chuỗi teacher hoặc student. Chèn admin vào sẽ bị từ chối. Không điền gì thì tự thành student.
Ưu điểm: dữ liệu luôn sạch, không có chuyện chỗ ghi Teacher chỗ ghi teacher.
Nhược điểm: muốn thêm vai trò mới phải sửa cấu trúc bảng, mà ALTER TABLE trên bảng lớn thì chậm.
EzCode dùng enum ở ba chỗ, và một trong ba có điểm lạ:
`role` enum('teacher','student','') NOT NULL DEFAULT 'student'
Đây là cột role của bảng enrollments. Nó có thêm giá trị rỗng '' ở cuối. Gần như chắc chắn là dấu vết của một lần chỉnh sửa cấu trúc, không có ý nghĩa gì. Bạn cứ giữ nguyên.
3. Áp dụng vào EzCode
3.1. Bảng users
📁
ezcode.sql— đọc trước
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(225) DEFAULT NULL,
`email` varchar(225) DEFAULT NULL,
`password` varchar(225) DEFAULT NULL,
`phone` varchar(10) DEFAULT NULL,
`role` enum('teacher','student') NOT NULL DEFAULT 'student'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Điểm quan trọng nhất: chỉ có một bảng users cho cả học viên lẫn giảng viên. Cột role phân biệt hai loại.
Cách này gọn hơn hẳn so với tạo hai bảng riêng. Nếu tách students và teachers, bạn sẽ phải viết mọi thứ hai lần: hai form đăng nhập, hai truy vấn, và bảng enrollments sẽ không biết trỏ tới bảng nào.
Cột password là varchar(225). Bạn có thể thắc mắc: mật khẩu đâu dài tới 225 ký tự? Đúng, nhưng cột này không lưu mật khẩu — nó lưu chuỗi băm. Chuỗi băm do password_hash() sinh ra dài đúng 60 ký tự và bắt đầu bằng $2y$10$. Bài 14 sẽ dạy.
Cột phone là varchar(10) chứ không phải int. Đúng, vì số điện thoại bắt đầu bằng số 0 — lưu kiểu số sẽ mất số 0 đầu, 0941280072 thành 941280072.
CHARSET=utf8mb4 cho phép lưu tiếng Việt có dấu và cả emoji. Nếu bạn từng thấy tên hiện thành Nguyá»…n Văn A thì đó là lỗi bảng chữ, và utf8mb4 là cách tránh.
3.2. Bảng courses
📁
ezcode.sql— đọc trước
CREATE TABLE `courses` (
`id` int(11) NOT NULL,
`teacher_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
`title` varchar(225) NOT NULL,
`description` text DEFAULT NULL,
`price` int(11) NOT NULL,
`image` varchar(255) NOT NULL,
`status` tinyint(4) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Đây là bảng trung tâm của cả website.
Hai khóa ngoại ở đầu: teacher_id trỏ tới users.id, category_id trỏ tới categories.id.
Cột status kiểu tinyint dùng như đúng/sai: 1 là hiện, 0 là ẩn. MySQL không có kiểu boolean riêng nên người ta dùng tinyint.
Cột này xuất hiện trong hầu hết mọi câu truy vấn của Models/Courses.php dưới dạng WHERE status = 1. Nghĩa là khóa học ẩn sẽ biến mất khỏi trang chủ và trang danh sách, nhưng giảng viên vẫn quản lý được.
⚠️ Lưu ý trước: chính điều kiện
status = 1này gây ra một lỗi ở Bài 18. Phương thứcdeleteCourse()gọiCourses::getById()vốn lọcstatus = 1, nên khóa học đang ẩn thì không xóa được. Đây là lỗi số 8 trong Bài 22.
Cột image là varchar(255) — lưu tên file, không lưu nội dung ảnh. Ảnh thật nằm trong thư mục public/img/courses/. Đây là cách làm chuẩn: database lưu văn bản, hệ thống file lưu file.
3.3. Bảng enrollments
📁
ezcode.sql— đọc trước
CREATE TABLE `enrollments` (
`user_id` int(11) NOT NULL,
`course_id` int(11) NOT NULL,
`role` enum('teacher','student','') NOT NULL DEFAULT 'student',
`price` int(11) NOT NULL,
`enrolled_at` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Đây là bảng trung gian của quan hệ nhiều-nhiều ở mục 2.7.
Không có cột id. Khóa chính là cặp (user_id, course_id).
Cột price lưu giá tại thời điểm mua, như đã giải thích ở mục 2.7. Toàn bộ phần thống kê doanh thu ở Bài 20 dựa vào cột này chứ không lấy giá hiện tại của khóa học.
Cột enrolled_at có DEFAULT current_timestamp() — MySQL tự điền thời điểm chèn dòng. Nhưng Models/User.php vẫn viết NOW() thẳng vào câu INSERT:
INSERT INTO enrollments (user_id, course_id, price, enrolled_at) VALUES (?, ?, ?, NOW())
Thừa nhưng vô hại. Bài 21 sẽ gặp lại.
3.4. Toàn cảnh 6 bảng
| Bảng | Số cột | Vai trò | Nối với |
|---|---|---|---|
users |
6 | Học viên và giảng viên chung một bảng | Được courses và enrollments trỏ tới |
categories |
2 | Danh mục khóa học, 33 dòng dữ liệu mẫu | Được courses trỏ tới |
courses |
8 | Khóa học — bảng trung tâm | Trỏ tới users và categories |
lessions |
6 | Bài học trong khóa | Trỏ tới courses |
enrollments |
5 | Bảng trung gian: ai đã mua khóa nào | Trỏ tới users và courses |
payments |
9 | Lịch sử giao dịch VNPay | Trỏ tới users và courses, có CASCADE |
Vì sao cần cả enrollments lẫn payments? Hai bảng này trông có vẻ trùng lặp nhưng trả lời hai câu hỏi khác nhau:
paymentstrả lời "đã có những giao dịch nào" — kể cả giao dịch thất bại, đang chờ, hoặc bị hủy.enrollmentstrả lời "ai được quyền học khóa nào" — chỉ có khi thanh toán thành công.
Một dòng payments với status = 'failed' sẽ không sinh ra dòng enrollments nào. Bài 21 sẽ dựng luồng đó.
Một điểm cần biết về tên bảng: lessions là viết sai chính tả của lessons. Tác giả gõ nhầm ngay từ đầu và giữ nguyên trong cả tên bảng lẫn tên file Models/Lession.php. Bạn phải gõ y hệt cái sai đó, vì mọi câu truy vấn trong project đều dùng tên này. Sửa cho đúng chính tả là code không chạy.
4. Chạy thử
Đây là bước bắt buộc. Không có database thì 15 bài sau không chạy được.
Bước 1 — Mở phpMyAdmin. Bật XAMPP, chắc chắn cả Apache và MySQL đều nền xanh, rồi vào http://localhost/phpmyadmin.
Bước 2 — Tạo database. Bấm tab Databases ở trên cùng. Trong ô Database name gõ:
ezcode
Ở ô collation bên cạnh, chọn utf8mb4_general_ci. Bấm Create.
Tên phải đúng là
ezcodeviết thường. FileModels/Database.phpmà bạn sẽ viết ở Bài 8 có dòngprivate $dbname = "ezcode";. Đặt tên khác là không kết nối được.
Bước 3 — Nạp dữ liệu. Bấm vào database ezcode vừa tạo ở danh sách bên trái. Sang tab Import. Bấm Choose File và chọn file ezcode.sql ở gốc project. Kéo xuống cuối trang bấm Import (hoặc Go).
Bước 4 — Kiểm chứng. Bấm lại vào ezcode ở cột bên trái. Bạn phải thấy đúng 6 bảng với số dòng như sau:
| Bảng | Số dòng phải có |
|---|---|
categories |
33 |
courses |
14 |
enrollments |
2 |
lessions |
4 |
payments |
8 |
users |
35 |
Bước 5 — Nhìn dữ liệu thật. Bấm vào bảng courses, sang tab Browse. Bạn thấy 14 khóa học với id từ 1 đến 12, rồi 20 và 21. Số 13 đến 19 bị thiếu vì đã bị xóa trước khi xuất file.
Hãy nhìn dòng đầu tiên và đối chiếu với mảng PHP ở mục 2.1 — chúng là cùng một thứ, chỉ khác cách trình bày.
Nếu Import báo lỗi:
Unknown database 'ezcode'— bạn quên bấm vào database trước khi Import.#1064 syntax error— file.sqlbị hỏng khi tải về. Hãy lấy lại file gốc.File is too large— không xảy ra với file này vì nó chỉ khoảng 20KB.
5. Bài tập
Ba bài tập này làm trực tiếp trong phpMyAdmin, tab SQL của database ezcode. Đây cũng là nơi bạn sẽ thử nghiệm mọi câu truy vấn trước khi đưa vào code PHP ở các bài sau.
Bài tập 1
Viết câu lệnh lấy tất cả khóa học có giá lớn hơn 400.000, sắp xếp theo giá giảm dần.
Cho biết có bao nhiêu khóa học thỏa mãn, và khóa nào đắt nhất.
Gợi ý cú pháp:
SELECT * FROM ten_bang WHERE dieu_kien ORDER BY cot DESC;
Bài tập 2
Đếm xem mỗi category_id có bao nhiêu khóa học. Kết quả phải có hai cột: mã danh mục và số lượng.
Gợi ý: dùng
GROUP BY category_idvà hàmCOUNT(*). Đặt tên cho cột đếm bằngAS so_luong.
Bài tập 3
Mở bảng users ở tab Browse và nhìn kỹ cột password của 35 dòng.
Trả lời ba câu:
- Mật khẩu của các dòng từ
id1 đến 33 trông như thế nào? Của dòng 34 và 35 trông như thế nào? - Trong hai nhóm đó, nhóm nào là chuỗi băm thật do
password_hash()sinh ra? - Theo bạn, những tài khoản nào thực sự đăng nhập được vào EzCode? Vì sao?
6. Đáp án
Đáp án bài tập 1
SELECT * FROM courses
WHERE price > 400000
ORDER BY price DESC;
Kết quả gồm 6 khóa học:
| id | title | price |
|---|---|---|
| 10 | Blockchain cơ bản | 800000 |
| 9 | DevOps cho lập trình viên | 700000 |
| 4 | Nhập môn Machine Learning | 600000 |
| 11 | Internet of Things | 600000 |
| 5 | An toàn thông tin nâng cao | 550000 |
| 2 | Flutter cho người mới bắt đầu | 500000 |
| 6 | Thiết kế UI/UX | 500000 |
Đắt nhất là Blockchain cơ bản với 800.000.
Chú ý DESC nghĩa là giảm dần (descending), ASC là tăng dần (ascending). Không ghi gì thì mặc định là ASC.
Câu này chính là bản rút gọn của một nhánh trong Courses::filter() mà bạn sẽ viết ở Bài 12:
case 'price_desc':
$sql .= " ORDER BY c.price DESC";
break;
Đáp án bài tập 2
SELECT category_id, COUNT(*) AS so_luong
FROM courses
GROUP BY category_id
ORDER BY so_luong DESC;
Kết quả: danh mục số 1 có 3 khóa học, các danh mục còn lại mỗi cái 1 khóa.
Cách đọc GROUP BY: nó gom các dòng có cùng category_id lại thành một nhóm, rồi COUNT(*) đếm số dòng trong mỗi nhóm. Không có GROUP BY thì COUNT(*) sẽ trả về một con số duy nhất là tổng số dòng của cả bảng.
GROUP BY là công cụ chính của toàn bộ phần thống kê ở Bài 19 và Bài 20. Ví dụ câu này trong Models/Courses.php:
SELECT DATE_FORMAT(e.enrolled_at, '%Y-%m') as month, COUNT(DISTINCT e.user_id) as students
FROM enrollments e
GROUP BY DATE_FORMAT(e.enrolled_at, '%Y-%m')
Cùng một ý tưởng, chỉ khác là gom theo tháng thay vì theo danh mục.
Đáp án bài tập 3
Câu 1. Dòng id từ 1 đến 33 có mật khẩu là những chuỗi ngắn dễ đọc: hashedpassword1, hashedpw4, hashedpw20... Dòng 34 và 35 có mật khẩu là chuỗi dài 60 ký tự khó đọc:
$2y$10$9bgjTjuatn1OhdtuaygleepnGSYjvGPkZgQXoflzqfhA8S15zaNl2
$2y$10$mo32fm4InqymyLoF2Fyu6O/u0CLgPg3Ixmf7vDKIf3V89dcooIcGW
Câu 2. Chỉ dòng 34 và 35 là chuỗi băm thật.
Dấu hiệu nhận biết: chuỗi băm của password_hash() với thuật toán mặc định luôn bắt đầu bằng $2y$10$ và dài đúng 60 ký tự.
$2y$— báo thuật toán bcrypt10— hệ số công việc, càng cao càng khó bẻ nhưng càng chậm- Phần còn lại là muối (salt) ngẫu nhiên cộng với chuỗi băm
Còn hashedpassword1 chỉ là chữ tiếng Anh do người tạo dữ liệu mẫu gõ tay cho có.
Câu 3. Chỉ hai tài khoản đăng nhập được:
| Mật khẩu | Vai trò | |
|---|---|---|
hhoang02052004@gmail.com |
12345abcd |
student |
nguyenhuyhoang2004k3a@gmail.com |
12345 |
teacher |
Đây đúng là hai tài khoản mẫu ghi trong README.md của project.
Vì sao 33 tài khoản còn lại không đăng nhập được? Vì UserController::login() kiểm tra bằng:
if ($user && password_verify($password, $user['password'])) {
password_verify() nhận vào mật khẩu người dùng gõ và chuỗi băm trong database. Nó đọc phần muối từ chuỗi băm, băm lại mật khẩu vừa gõ bằng đúng muối đó, rồi so hai kết quả.
Nhưng hashedpassword1 không phải chuỗi băm hợp lệ — nó không có phần muối, không có định dạng $2y$. password_verify() không đọc được gì và trả về false với mọi mật khẩu bạn nhập.
Kể cả khi bạn gõ đúng chữ hashedpassword1 vào ô mật khẩu, nó vẫn thất bại.
Bài học rút ra: dữ liệu mẫu và dữ liệu thật khác nhau. Khi bạn thử nghiệm EzCode từ Bài 15 trở đi, chỉ dùng được hai tài khoản trên, hoặc tự đăng ký tài khoản mới ở Bài 14 — lúc đó password_hash() sẽ tạo chuỗi băm thật cho bạn.
➡️ Bài tiếp theo: Bài 8 — PDO và lớp Database
⬅️ Về mục lục · Bài trước
All rights reserved