# Hybrid Role Architecture - Hướng dẫn sử dụng ## 📋 Tổng quan Hệ thống sử dụng **Hybrid Role Architecture** để tối ưu hiệu năng: - **80-90% users (học sinh, phụ huynh)**: Dùng `primary_role_info` trong `UserProfile` → **NHANH** (2 JOINs) - **10-20% users (giáo viên, quản lý)**: Dùng `UserAssignment` → **Linh hoạt** (5 JOINs) ## 🗂️ Cấu trúc dữ liệu ### UserProfile.primary_role_info (JSON) ```json { "role_id": "uuid", "role_code": "student", "role_name": "Học sinh", "school": { "id": "uuid", "name": "SENA Hà Nội" }, "class": { "id": "uuid", "name": "K1A" }, "grade": { "id": "uuid", "name": "Khối 1" }, "student_code": "HS001", "enrollment_date": "2024-09-01", "status": "active" } ``` ## 🚀 Migration & Setup ### 1. Thêm column vào database ```bash node scripts/add-primary-role-info.js ``` ### 2. Populate dữ liệu cho học sinh hiện có ```bash node scripts/populate-primary-role-info.js ``` ### 3. Kiểm tra kết quả ```sql SELECT user_id, full_name, JSON_EXTRACT(primary_role_info, '$.role_code') as role, JSON_EXTRACT(primary_role_info, '$.school.name') as school FROM user_profiles WHERE primary_role_info IS NOT NULL LIMIT 10; ``` ## 📝 API Response mới ### Login Response ```json { "success": true, "message": "Đăng nhập thành công", "data": { "token": "eyJhbGc...", "user": { "id": "uuid", "username": "student001", "email": "student@example.com", "profile": { "full_name": "Nguyễn Văn A", "avatar_url": "https://...", "primary_role_info": { "role_code": "student", "role_name": "Học sinh", "school": { "id": "uuid", "name": "SENA HN" }, "class": { "id": "uuid", "name": "K1A" } } }, "role": { "role_id": "uuid", "role_code": "student", "role_name": "Học sinh", "school": { "id": "uuid", "name": "SENA HN" }, "class": { "id": "uuid", "name": "K1A" } }, "permissions": [ { "code": "view_own_grades", "name": "Xem điểm của mình", "resource": "grades", "action": "view" } ] } } } ``` ## 🔧 Khi nào dùng cái nào? ### ✅ Dùng `primary_role_info` (Fast Path) - Học sinh (`student`) - Phụ huynh thường (`parent`) - User có 1 role cố định - Không thay đổi school/class thường xuyên **Cách tạo:** ```javascript await UserProfile.create({ user_id: userId, full_name: "Nguyễn Văn A", primary_role_info: { role_id: studentRoleId, role_code: "student", role_name: "Học sinh", school: { id: schoolId, name: "SENA HN" }, class: { id: classId, name: "K1A" }, } }); ``` ### ✅ Dùng `UserAssignment` (Flexible Path) - Giáo viên dạy nhiều trường - Quản lý nhiều center - User có nhiều role đồng thời - Cần theo dõi thời gian hiệu lực role **Cách tạo:** ```javascript // Set primary_role_info = null await UserProfile.create({ user_id: userId, full_name: "Nguyễn Thị B", primary_role_info: null, // ← Để null }); // Tạo assignments await UserAssignment.bulkCreate([ { user_id: userId, role_id: teacherRoleId, school_id: school1Id, is_primary: true, }, { user_id: userId, role_id: teacherRoleId, school_id: school2Id, is_primary: false, } ]); ``` ## ⚡ Performance ### Before (Tất cả user dùng UserAssignment) - Login query: **6 JOINs** - Avg response time: **50-80ms** (without cache) ### After (Hybrid approach) - **Student login**: **2 JOINs** → **20-30ms** ✅ (-60%) - **Teacher login**: **5 JOINs** → **50-70ms** (tương tự) ### Với Redis Cache - **Student**: **< 1ms** (cache hit) - **Teacher**: **< 2ms** (cache hit) ## 🔄 Cập nhật primary_role_info Khi học sinh chuyển lớp hoặc thay đổi thông tin: ```javascript // Option 1: Update trực tiếp await userProfile.update({ primary_role_info: { ...userProfile.primary_role_info, class: { id: newClassId, name: "K2A" } } }); // Option 2: Rebuild từ StudentDetail const student = await StudentDetail.findOne({ where: { user_id: userId }, include: [ { model: Class, as: 'currentClass', include: [School] }, ] }); await userProfile.update({ primary_role_info: { role_id: studentRole.id, role_code: 'student', role_name: 'Học sinh', school: { id: student.currentClass.school.id, name: student.currentClass.school.school_name }, class: { id: student.currentClass.id, name: student.currentClass.class_name } } }); // Don't forget to invalidate cache! await redis.del(`user:${userId}:permissions`); ``` ## 🎯 Best Practices 1. **Luôn check `primary_role_info` trước** khi query `UserAssignment` 2. **Cache permissions** trong Redis với TTL 1-24h 3. **Invalidate cache** khi update role/permission 4. **Monitor slow queries** để phát hiện N+1 query 5. **Index properly**: `user_id`, `role_id`, `school_id` trong `user_assignments` ## 📊 Monitoring ```sql -- Thống kê users theo role strategy SELECT CASE WHEN primary_role_info IS NOT NULL THEN 'Fast Path' ELSE 'Flexible Path' END as strategy, COUNT(*) as count, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM user_profiles), 2) as percentage FROM user_profiles GROUP BY strategy; -- Expected result: -- Fast Path: 80-90% (students, parents) -- Flexible Path: 10-20% (teachers, managers) ``` ## 🔐 Security - JWT token bây giờ bao gồm `roleCode` để validate nhanh - Permissions vẫn được check từ database (không tin JWT hoàn toàn) - Rate limiting theo role: student (100 req/min), teacher (200 req/min) ## 🐛 Troubleshooting ### User không có role sau login? ```javascript // Check primary_role_info const profile = await UserProfile.findOne({ where: { user_id } }); console.log(profile.primary_role_info); // Check UserAssignment const assignments = await UserAssignment.findAll({ where: { user_id, is_active: true } }); console.log(assignments); ``` ### Query chậm? 1. Check indexes: `SHOW INDEX FROM user_profiles;` 2. Check Redis cache hit rate 3. Enable query logging: `SET GLOBAL general_log = 'ON';` --- **Last updated:** January 19, 2026 **Version:** 2.0.0