6.3 KiB
6.3 KiB
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_infotrongUserProfile→ 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)
{
"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
node scripts/add-primary-role-info.js
2. Populate dữ liệu cho học sinh hiện có
node scripts/populate-primary-role-info.js
3. Kiểm tra kết quả
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
{
"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:
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:
// 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:
// 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
- Luôn check
primary_role_infotrước khi queryUserAssignment - Cache permissions trong Redis với TTL 1-24h
- Invalidate cache khi update role/permission
- Monitor slow queries để phát hiện N+1 query
- Index properly:
user_id,role_id,school_idtronguser_assignments
📊 Monitoring
-- 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?
// 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?
- Check indexes:
SHOW INDEX FROM user_profiles; - Check Redis cache hit rate
- Enable query logging:
SET GLOBAL general_log = 'ON';
Last updated: January 19, 2026
Version: 2.0.0