This commit is contained in:
307
LESSON_STORY_GUIDE.md
Normal file
307
LESSON_STORY_GUIDE.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user