308 lines
6.3 KiB
Markdown
308 lines
6.3 KiB
Markdown
# 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)
|