Files
sena_db_api_layer/LESSON_STORY_GUIDE.md
Ken 6287a019e3
All checks were successful
Deploy to Production / deploy (push) Successful in 20s
update
2026-02-27 09:38:39 +07:00

6.3 KiB

Lesson-Story N:N Relationship Guide

📋 Tổng quan

Quan hệ Many-to-Many (N:N) giữa LessonStory:

  • 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 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

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_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

// 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

  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