1. Ràng buộc khóa ngoại (Referential Integrity)

Tất cả quan hệ giữa các bảng được bảo vệ bằng FOREIGN KEY constraints — SQL Server sẽ từ chối các thao tác vi phạm quan hệ.

Các quan hệ cốt lõi

t_huca_faculties ──────────────────────────────┐
t_huca_academic_years ─────────────────────────┤
t_huca_provinces ──────────────────────────────┤

                                    t_huca_users (Bảng trung tâm)

                ┌──────────┬──────────┬────────┴──────────┬──────────┐
                ▼          ▼          ▼                   ▼          ▼
          t_huca_posts  t_huca_  t_huca_events    t_huca_      t_huca_
                        groups                  donations   mentors

Danh sách FK quan trọng

Bảng conTrường FKBảng cha
t_huca_usersFacultyIdt_huca_faculties
t_huca_usersAcademicYearIdt_huca_academic_years
t_huca_usersClassIdt_huca_classes
t_huca_usersProvinceIdt_huca_provinces
t_huca_classesFacultyIdt_huca_faculties
t_huca_classesAcademicYearIdt_huca_academic_years
t_huca_postsGroupIdt_huca_groups
t_huca_postsAuthorIdt_huca_users
t_huca_event_registrationsEventIdt_huca_events
t_huca_event_registrationsUserIdt_huca_users
t_huca_donationsCampaignIdt_huca_fundraising_campaigns
t_huca_job_applicationsJobIdt_huca_jobs

2. Ràng buộc duy nhất (Unique Constraints)

Mức bản ghi đơn (Single-column UNIQUE)

BảngCộtÝ nghĩa
t_huca_usersEmail1 email = 1 tài khoản
t_huca_usersPhoneNumber1 SĐT = 1 tài khoản
t_huca_facultiesCodeMã khoa không trùng
t_huca_classesCodeMã lớp không trùng
t_huca_academic_yearsYearNumberSố khóa không trùng
t_huca_certificatesCertificateCodeMã chứng chỉ duy nhất
t_huca_vouchersCodeMã voucher duy nhất
t_huca_badgesCodeMã huy hiệu duy nhất
t_huca_mentorsUserId1 người = 1 hồ sơ mentor

Mức composite (Multi-column UNIQUE)

BảngCộtÝ nghĩa
t_huca_event_registrations(EventId, UserId)Mỗi người đăng ký 1 lần/sự kiện
t_huca_group_members(GroupId, UserId)Mỗi người tham gia 1 lần/nhóm
t_huca_reactions(UserId, TargetType, TargetId)Mỗi người react 1 lần
t_huca_user_roles(UserId, RoleId)Không gán trùng vai trò
t_huca_committee_members(CommitteeId, TermId, UserId)1 chức danh/người/nhiệm kỳ
t_huca_company_members(CompanyId, UserId)Mỗi người 1 lần/công ty
t_huca_job_applications(JobId, ApplicantId)Ứng tuyển 1 lần/tin
t_huca_user_badges(UserId, BadgeId)Mỗi huy hiệu 1 lần/người
t_huca_user_locationsUserId1 bản ghi vị trí/người
t_huca_program_registrations(ProgramId, UserId)Đăng ký 1 lần/chương trình

3. Soft Delete

Tất cả bảng nghiệp vụ sử dụng IsActive BIT DEFAULT 1 thay vì xóa vật lý:
-- KHÔNG bao giờ xóa như thế này:
DELETE FROM t_huca_users WHERE Id = @UserId;

-- Luôn dùng soft delete:
UPDATE t_huca_users SET IsActive = 0, UpdatedAt = GETUTCDATE()
WHERE Id = @UserId;
Lợi ích:
  • Giữ nguyên tính toàn vẹn FK (không phá vỡ quan hệ)
  • Dữ liệu lịch sử được bảo toàn
  • Có thể phục hồi khi cần
  • Audit trail đầy đủ

4. Chiến lược Indexing

Index trên Foreign Keys

Tất cả cột FK đều có index để tránh table scan khi JOIN:
CREATE INDEX IX_users_FacultyId ON t_huca_users(FacultyId);
CREATE INDEX IX_users_ClassId ON t_huca_users(ClassId);
CREATE INDEX IX_posts_GroupId ON t_huca_posts(GroupId);
CREATE INDEX IX_posts_AuthorId ON t_huca_posts(AuthorId);
-- ... (đã tạo đầy đủ trong file SQL)

Index cho truy vấn thường gặp

-- Tìm kiếm bài viết mới nhất
CREATE INDEX IX_posts_CreatedAt ON t_huca_posts(CreatedAt DESC);

-- Tìm thông báo chưa đọc
CREATE INDEX IX_notifications_UserId ON t_huca_notifications(UserId, IsRead, CreatedAt DESC);

-- Bảng xếp hạng
CREATE INDEX IX_leaderboards_Period ON t_huca_leaderboards(Period, PeriodValue, TotalPoints DESC);

Filtered Index

-- Chỉ index bài viết nổi bật (tiết kiệm không gian)
CREATE INDEX IX_articles_IsFeatured ON t_huca_articles(IsFeatured)
WHERE IsFeatured = 1;

5. Denormalization có kiểm soát

Một số trường counter được denormalize để tăng tốc đọc:
BảngTrườngNguồn thực
t_huca_postsLikeCount, CommentCountCOUNT từ reactions/comments
t_huca_groupsMemberCountCOUNT từ group_members
t_huca_fundraising_campaignsCurrentAmount, DonorCountSUM/COUNT từ donations
t_huca_jobsApplicationCount, ViewCountCOUNT từ job_applications
:::warning Đồng bộ dữ liệu Các trường counter phải được cập nhật đồng bộ trong cùng transaction với thao tác chính:
BEGIN TRANSACTION;
    INSERT INTO t_huca_reactions (UserId, TargetType, TargetId, ReactionType) VALUES (...);
    UPDATE t_huca_posts SET LikeCount = LikeCount + 1 WHERE Id = @PostId;
COMMIT;
:::

6. Collation Tiếng Việt

Database sử dụng Vietnamese_CI_AS để hỗ trợ đầy đủ ký tự tiếng Việt:
CREATE DATABASE HucaDB COLLATE Vietnamese_CI_AS;
  • CI = Case Insensitive (không phân biệt hoa/thường)
  • AS = Accent Sensitive (phân biệt dấu: “a” ≠ “à” ≠ “á” ≠ “ã” ≠ “ả” ≠ “ạ”)
:::tip Tìm kiếm không dấu Để hỗ trợ tìm kiếm không dấu (VD: “Nguyen” tìm ra “Nguyễn”), có thể sử dụng collation Vietnamese_CI_AI ở cấp column hoặc dùng thư viện chuẩn hóa chuỗi ở tầng application. :::

7. Audit Log

Bảng t_huca_audit_logs ghi nhận mọi thay đổi quan trọng:
-- Ví dụ log khi admin phê duyệt tài khoản
INSERT INTO t_huca_audit_logs (UserId, Action, EntityType, EntityId, OldValues, NewValues, IpAddress)
VALUES (
    @AdminId,
    'ApproveUser',
    'User',
    @TargetUserId,
    '{"IsApproved": false}',
    '{"IsApproved": true}',
    @IpAddress
);
Truy vấn lịch sử thay đổi:
-- Xem lịch sử hoạt động của 1 user
SELECT Action, EntityType, EntityId, OldValues, NewValues, IpAddress, CreatedAt
FROM t_huca_audit_logs
WHERE UserId = @UserId
ORDER BY CreatedAt DESC;

-- Kiểm tra tất cả thay đổi trên 1 bản ghi
SELECT UserId, Action, OldValues, NewValues, CreatedAt
FROM t_huca_audit_logs
WHERE EntityType = 'User' AND EntityId = @TargetUserId
ORDER BY CreatedAt DESC;