Files
sena_db_api_layer/HYBRID_ROLE_ARCHITECTURE.md
silverpro89 97e2e8402e update
2026-01-19 20:32:23 +07:00

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_info trong UserProfileNHANH (2 JOINs)
  • 10-20% users (giáo viên, quản lý): Dùng UserAssignmentLinh 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 JOINs20-30ms (-60%)
  • Teacher login: 5 JOINs50-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

  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

-- 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?

  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