# 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 ```sql 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 lesson - `is_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 ```http GET /api/lessons/:lessonId/stories?page=1&limit=20 ``` **Response:** ```json { "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 ```http 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 ```http PUT /api/lessons/:lessonId/stories/:storyId Content-Type: application/json { "display_order": 2, "is_required": false } ``` #### Xóa story khỏi lesson ```http 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 ```http GET /api/stories/:storyId/lessons?page=1&limit=20 ``` **Response:** ```json { "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 ```http 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 ```http DELETE /api/stories/:storyId/lessons/:lessonId ``` ## 💡 Use Cases ### Use Case 1: Tạo lesson với nhiều stories ```bash # 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 ```bash # 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 ```bash # Đổ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_order` bắ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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript 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 1. **Index**: lesson_id và story_id đều có index 2. **Pagination**: Luôn dùng limit khi query 3. **Cache**: Cache kết quả 30 phút (1800s) 4. **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 ## 📚 Related Documents - [API_STRUCTURE_GUIDE.md](../API_STRUCTURE_GUIDE.md) - [STORY_GUIDE.md](../STORY_GUIDE.md) - [SUBJECT_CHAPTER_LESSON_GUIDE.md](../SUBJECT_CHAPTER_LESSON_GUIDE.md)