6.3 KiB
6.3 KiB
Lesson-Story N:N Relationship Guide
📋 Tổng quan
Quan hệ Many-to-Many (N:N) giữa Lesson và Story:
- Một lesson có thể chứa nhiều stories
- Một story có thể được sử dụng trong nhiều lessons
- Quản lý qua bảng pivot:
lesson_stories
🗂️ Database Schema
Bảng: lesson_stories
CREATE TABLE lesson_stories (
id CHAR(36) PRIMARY KEY,
lesson_id CHAR(36) NOT NULL,
story_id CHAR(36) NOT NULL,
display_order INT DEFAULT 0,
is_required BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(lesson_id, story_id)
);
Metadata trong Pivot Table
display_order: Thứ tự hiển thị story trong lessonis_required: Story có bắt buộc phải hoàn thành không
🔌 API Endpoints
1. Quản lý từ phía Lesson
Lấy danh sách stories trong lesson
GET /api/lessons/:lessonId/stories?page=1&limit=20
Response:
{
"success": true,
"data": {
"lesson": {
"id": "xxx",
"lesson_title": "Alphabet A-F",
"lesson_number": 1
},
"stories": [
{
"id": "yyy",
"name": "The Greedy Cat",
"type": "story",
"display_order": 1,
"is_required": true
}
],
"pagination": { ... }
}
}
Thêm story vào lesson
POST /api/lessons/:lessonId/stories
Content-Type: application/json
{
"story_id": "story-uuid",
"display_order": 1,
"is_required": true
}
Cập nhật story trong lesson
PUT /api/lessons/:lessonId/stories/:storyId
Content-Type: application/json
{
"display_order": 2,
"is_required": false
}
Xóa story khỏi lesson
DELETE /api/lessons/:lessonId/stories/:storyId
2. Quản lý từ phía Story (Alternative)
Lấy danh sách lessons sử dụng story
GET /api/stories/:storyId/lessons?page=1&limit=20
Response:
{
"success": true,
"data": {
"story": {
"id": "yyy",
"name": "The Greedy Cat",
"type": "story"
},
"lessons": [
{
"id": "xxx",
"lesson_title": "Alphabet A-F",
"display_order": 1,
"is_required": true
}
],
"pagination": { ... }
}
}
Thêm lesson vào story
POST /api/stories/:storyId/lessons
Content-Type: application/json
{
"lesson_id": "lesson-uuid",
"display_order": 1,
"is_required": true
}
Xóa lesson khỏi story
DELETE /api/stories/:storyId/lessons/:lessonId
💡 Use Cases
Use Case 1: Tạo lesson với nhiều stories
# 1. Tạo lesson
POST /api/chapters/:chapterId/lessons
{
"lesson_number": 1,
"lesson_title": "Learning ABC",
"lesson_type": "json_content"
}
# 2. Thêm story thứ nhất
POST /api/lessons/{lesson_id}/stories
{
"story_id": "story-1-id",
"display_order": 1,
"is_required": true
}
# 3. Thêm story thứ hai
POST /api/lessons/{lesson_id}/stories
{
"story_id": "story-2-id",
"display_order": 2,
"is_required": false
}
Use Case 2: Tái sử dụng story cho nhiều lessons
# Story "The Greedy Cat" được sử dụng trong 3 lessons khác nhau
# Lesson 1 - Grade 1
POST /api/lessons/lesson-1-id/stories
{
"story_id": "greedy-cat-id",
"display_order": 1,
"is_required": true
}
# Lesson 2 - Grade 2
POST /api/lessons/lesson-2-id/stories
{
"story_id": "greedy-cat-id",
"display_order": 2,
"is_required": false
}
# Lesson 3 - Review lesson
POST /api/lessons/lesson-3-id/stories
{
"story_id": "greedy-cat-id",
"display_order": 1,
"is_required": true
}
Use Case 3: Cập nhật thứ tự stories trong lesson
# Đổi thứ tự từ 1 sang 3
PUT /api/lessons/{lesson_id}/stories/{story_id}
{
"display_order": 3
}
# Đánh dấu không bắt buộc
PUT /api/lessons/{lesson_id}/stories/{story_id}
{
"is_required": false
}
🎯 Best Practices
1. Naming Convention
display_orderbắt đầu từ 1, tăng dần- Để lại khoảng trống giữa các order (1, 5, 10...) để dễ insert sau
2. Validation
// Check duplicate trước khi thêm
const existing = await LessonStory.findOne({
where: { lesson_id, story_id }
});
if (existing) {
return res.status(400).json({
message: 'Story đã tồn tại trong lesson này'
});
}
3. Cache Strategy
// Clear cache của cả 2 phía khi có thay đổi
await cacheUtils.deletePattern(`lesson:${lessonId}:stories:*`);
await cacheUtils.deletePattern(`story:${storyId}:lessons:*`);
4. Query Optimization
// Sử dụng include để eager load
const lessons = await Lesson.findAll({
include: [{
model: Story,
as: 'stories',
through: {
attributes: ['display_order', 'is_required']
}
}]
});
⚠️ Important Notes
Cascade Delete
- Khi xóa lesson → Tự động xóa relationships trong
lesson_stories - Khi xóa story → Tự động xóa relationships trong
lesson_stories - KHÔNG xóa story/lesson khi xóa relationship
Transaction Safety
const transaction = await sequelize.transaction();
try {
// Add multiple stories to lesson
await LessonStory.bulkCreate([
{ lesson_id, story_id: 'story1', display_order: 1 },
{ lesson_id, story_id: 'story2', display_order: 2 },
], { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
Performance Tips
- Index: lesson_id và story_id đều có index
- Pagination: Luôn dùng limit khi query
- Cache: Cache kết quả 30 phút (1800s)
- Eager Loading: Load stories cùng lesson khi cần
🔍 Troubleshooting
Issue 1: Duplicate Error
Error: Unique constraint violation
Solution: Story đã tồn tại trong lesson, kiểm tra trước khi thêm
Issue 2: Foreign Key Error
Error: Cannot add or update a child row
Solution: lesson_id hoặc story_id không tồn tại, validate trước
Issue 3: Order Conflict
Warning: Multiple stories have same display_order
Solution: Cập nhật display_order để unique trong lesson