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

279 lines
6.3 KiB
Markdown

# 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