在这里插入图片描述

📖 引言

在知识问答学习应用中,数据模型之间的关系设计是构建整个应用架构的基础。合理的数据模型关系不仅能够提高数据的组织效率,还能简化业务逻辑的实现,让代码更加清晰和可维护。

本文将详细讲解各个数据模型之间的关系设计,包括一对多、多对一、多对多等关系类型,以及数据流向和业务逻辑的组织。通过本文,你将掌握:

  • 如何设计和理解数据模型之间的关系
  • 如何使用 Mermaid 绘制实体关系图
  • 如何实现数据模型之间的关联查询
  • 如何在实际项目中维护数据一致性

🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解各个数据模型之间的关系类型
  • ✅ 掌握实体关系图(ER 图)的绘制方法
  • ✅ 实现数据模型之间的关联查询
  • ✅ 设计合理的数据流向和业务逻辑

💡 需求分析

功能模块设计

模块 功能描述 技术要点
实体关系梳理 梳理各模型之间的关系 ER 图、一对多、多对多
数据流向设计 设计数据的传递路径 服务层、数据层
关联查询实现 实现跨模型的查询逻辑 join、filter
一致性维护 保证数据的一致性 事务、更新逻辑

🛠️ 核心实现

步骤1: 数据模型整体架构

功能说明

首先,我们需要梳理整个应用中所有的数据模型,以及它们之间的关系。

完整代码
// models 目录下的所有模型

// User.ts - 用户模型
export interface User {
  id: string;
  nickname: string;
  avatar: string;
  level: number;
  totalScore: number;
  totalExperience: number;
  rank: number;
  consecutiveDays: number;
  lastLoginDate: string;
  createdAt: string;
  settings: UserSettings;
  statistics: UserStatistics;
  unlockedLevels: string[];
  completedLevels: string[];
  levelStars: Map<string, number>;
  achievements: string[];
  wrongQuestions: string[];
  favorites: string[];
  tools: UserTools;
  quizHistory: QuizRecord[];
  dailyRecords: DailyRecord[];
  ownedItems: string[];
}

// Question.ts - 题目模型
export interface Question {
  id: string;
  subjectId: string;
  levelId: string;
  type: QuestionType;
  content: string;
  options?: QuestionOption[];
  answer: string | string[];
  explanation: string;
  knowledgePoint: string;
  difficulty: number;
  source: string;
  createdAt: string;
  imageUrl?: string;
}

// Level.ts - 关卡模型
export interface Level {
  id: string;
  subjectId: string;
  subjectName: string;
  name: string;
  description: string;
  difficulty: Difficulty;
  difficultyLevel: number;
  order: number;
  unlockCondition: string;
  questionCount: number;
  timeLimit: number;
  rewards: LevelReward;
  icon: string;
  background: string;
}

// Subject.ts - 学科模型
export interface Subject {
  id: string;
  name: string;
  description: string;
  icon: string;
  color: string;
  levels: Level[];
}

// Achievement.ts - 成就模型
export interface Achievement {
  id: string;
  name: string;
  description: string;
  icon: string;
  type: AchievementType;
  condition: AchievementCondition;
  reward: AchievementReward;
  rarity: Rarity;
}

// QuizRecord.ts - 测验记录模型
export interface QuizRecord {
  id: string;
  userId: string;
  levelId: string;
  startTime: string;
  endTime: string;
  totalQuestions: number;
  correctQuestions: number;
  accuracy: number;
  score: number;
  experience: number;
  stars: number;
  timeUsed: number;
  questionResults: QuestionResult[];
  subjectId?: string;
  difficulty?: string;
  timestamp?: string;
}
代码解析

1. 数据模型列表

模型名称 主要字段 用途
User id, nickname, level, statistics 用户信息和学习数据
Question id, subjectId, levelId, content 题目内容和答案
Level id, subjectId, difficulty, order 关卡配置和解锁条件
Subject id, name, description, levels 学科分类和关卡集合
Achievement id, type, condition, reward 成就配置和解锁条件
QuizRecord id, userId, levelId, questionResults 测验记录和答题详情

步骤2: 绘制实体关系图

功能说明

使用 Mermaid 语法绘制实体关系图,展示各个模型之间的关系。

完整代码

has

references

contains

contains

belongs_to

has

references

unlocks

unlocked_by

unlocks

completes

USER

string

id

PK

string

nickname

number

level

number

totalScore

number

totalExperience

number

consecutiveDays

string

lastLoginDate

array

unlockedLevels

array

completedLevels

map

levelStars

array

achievements

array

wrongQuestions

array

favorites

QUIZ_RECORD

string

id

PK

string

userId

FK

string

levelId

FK

number

totalQuestions

number

correctQuestions

number

accuracy

number

score

number

experience

number

stars

number

timeUsed

timestamp

startTime

timestamp

endTime

LEVEL

string

id

PK

string

subjectId

FK

string

name

number

difficultyLevel

number

order

number

questionCount

number

timeLimit

QUESTION_RESULT

string

id

PK

string

quizRecordId

FK

string

questionId

FK

string

userAnswer

boolean

isCorrect

number

timeUsed

array

usedTools

QUESTION

string

id

PK

string

subjectId

FK

string

levelId

FK

QuestionType

type

string

content

array

options

string

answer

string

explanation

number

difficulty

SUBJECT

string

id

PK

string

name

string

description

string

color

ACHIEVEMENT

string

id

PK

AchievementType

type

AchievementCondition

condition

Rarity

rarity

AchievementReward

reward

代码解析

1. 关系类型说明

关系类型 符号 说明 示例
一对一 `
一对多 ` –o{`
多对一 `}o– `
多对多 }o--o{ 多个实体对应多个实体 User-Achievement

2. 主键和外键说明

// 主键 (PK) - 唯一标识一条记录
User.id  // 用户 ID

// 外键 (FK) - 关联其他表
QuizRecord.userId  // 关联 User
QuizRecord.levelId  // 关联 Level

步骤3: 核心关系详解

功能说明

详细讲解几个核心的一对多、多对一、多对多关系。

User 与 QuizRecord 的一对多关系
// User 模型中存储 QuizRecord 列表
export interface User {
  // ...
  quizHistory: QuizRecord[];  // 用户的所有测验记录
}

// 查询某个用户的所有测验记录
function getUserQuizHistory(user: User): QuizRecord[] {
  return user.quizHistory;
}

// 查询某个用户的最近 N 条记录
function getRecentQuizHistory(user: User, limit: number): QuizRecord[] {
  return user.quizHistory.slice(-limit);
}

// 按关卡分组统计
function getQuizStatsByLevel(user: User): Map<string, QuizRecord[]> {
  const grouped = new Map<string, QuizRecord[]>();
  
  for (const record of user.quizHistory) {
    const existing = grouped.get(record.levelId) || [];
    existing.push(record);
    grouped.set(record.levelId, existing);
  }
  
  return grouped;
}

关系说明:

  • 一个 User 可以有多条 QuizRecord
  • 每条 QuizRecord 属于一个 User
  • 通过 userId 字段关联
Level 与 Question 的多对一关系
// Level 模型
export interface Level {
  id: string;
  questionCount: number;  // 该关卡的题目数量
  // ...
}

// Question 模型
export interface Question {
  id: string;
  levelId: string;  // 关联的关卡 ID
  // ...
}

// 获取某个关卡的所有题目
function getQuestionsByLevel(questions: Question[], levelId: string): Question[] {
  return questions.filter(q => q.levelId === levelId);
}

// 获取关卡的随机题目(用于测验)
function getRandomQuestionsForLevel(
  questions: Question[], 
  levelId: string, 
  count: number
): Question[] {
  const levelQuestions = getQuestionsByLevel(questions, levelId);
  const shuffled = levelQuestions.sort(() => Math.random() - 0.5);
  return shuffled.slice(0, count);
}

关系说明:

  • 一个 Level 包含多个 Question
  • 每个 Question 属于一个 Level
  • 通过 levelId 字段关联
User 与 Achievement 的多对多关系
// User 模型
export interface User {
  achievements: string[];  // 已解锁的成就 ID 列表
  // ...
}

// 查询用户是否解锁了某个成就
function hasUnlockedAchievement(user: User, achievementId: string): boolean {
  return user.achievements.includes(achievementId);
}

// 获取用户已解锁的成就列表
function getUnlockedAchievements(
  user: User, 
  allAchievements: Achievement[]
): Achievement[] {
  return allAchievements.filter(a => user.achievements.includes(a.id));
}

// 解锁新成就
function unlockAchievement(user: User, achievementId: string): void {
  if (!user.achievements.includes(achievementId)) {
    user.achievements.push(achievementId);
  }
}

关系说明:

  • 一个 User 可以解锁多个 Achievement
  • 一个 Achievement 可以被多个 User 解锁
  • 通过 achievements 数组存储已解锁的成就 ID

步骤4: 数据流向设计

功能说明

设计数据在各模型之间的流向,以及业务逻辑的处理路径。

完整代码
/**
 * 数据流向图
 * 
 * 用户开始测验 → 选择关卡 → 获取题目 → 答题 → 保存记录 → 更新统计 → 检查成就
 */

// 1. 用户开始测验
function startQuiz(user: User, levelId: string): QuizSession {
  // 获取关卡信息
  const level = getLevelById(levelId);
  
  // 获取题目
  const questions = getRandomQuestionsForLevel(allQuestions, levelId, level.questionCount);
  
  // 创建测验会话
  const session: QuizSession = {
    id: generateQuizId(),
    userId: user.id,
    levelId: levelId,
    questions: questions,
    startTime: new Date().toISOString(),
    answers: [],
    currentIndex: 0
  };
  
  return session;
}

// 2. 提交答案
function submitAnswer(session: QuizSession, questionId: string, answer: string): void {
  const question = session.questions.find(q => q.id === questionId);
  const isCorrect = checkAnswer(question, answer);
  
  session.answers.push({
    questionId: questionId,
    userAnswer: answer,
    isCorrect: isCorrect,
    timeUsed: calculateTimeUsed(session)
  });
  
  session.currentIndex++;
}

// 3. 结束测验
function finishQuiz(user: User, session: QuizSession): QuizRecord {
  // 计算结果
  const correctCount = session.answers.filter(a => a.isCorrect).length;
  const totalCount = session.answers.length;
  const accuracy = correctCount / totalCount;
  const stars = calculateStars(accuracy);
  
  // 创建测验记录
  const record: QuizRecord = {
    id: session.id,
    userId: user.id,
    levelId: session.levelId,
    startTime: session.startTime,
    endTime: new Date().toISOString(),
    totalQuestions: totalCount,
    correctQuestions: correctCount,
    accuracy: accuracy,
    score: calculateScore(session.levelId, stars),
    experience: calculateExperience(session.levelId, stars),
    stars: stars,
    timeUsed: calculateTotalTimeUsed(session),
    questionResults: session.answers
  };
  
  // 保存记录到用户
  user.quizHistory.push(record);
  
  // 更新用户统计
  updateUserStatistics(user, record);
  
  // 检查成就
  checkAndUnlockAchievements(user);
  
  // 更新关卡完成状态
  updateLevelCompletion(user, session.levelId, stars);
  
  return record;
}

// 4. 更新用户统计
function updateUserStatistics(user: User, record: QuizRecord): void {
  user.statistics.totalQuestions += record.totalQuestions;
  user.statistics.correctQuestions += record.correctQuestions;
  user.statistics.averageAccuracy = 
    user.statistics.correctQuestions / user.statistics.totalQuestions;
  
  user.totalScore += record.score;
  user.totalExperience += record.experience;
}

// 5. 更新关卡完成状态
function updateLevelCompletion(user: User, levelId: string, stars: number): void {
  // 添加到已完成关卡
  if (!user.completedLevels.includes(levelId)) {
    user.completedLevels.push(levelId);
  }
  
  // 更新星级(保留最高星级)
  const currentStars = user.levelStars.get(levelId) || 0;
  if (stars > currentStars) {
    user.levelStars.set(levelId, stars);
  }
}

// 6. 检查并解锁成就
function checkAndUnlockAchievements(user: User): Achievement[] {
  const unlocked: Achievement[] = [];
  
  for (const achievement of allAchievements) {
    // 跳过已解锁的
    if (user.achievements.includes(achievement.id)) {
      continue;
    }
    
    // 检查条件
    if (checkAchievementCondition(user, achievement)) {
      user.achievements.push(achievement.id);
      user.totalScore += achievement.reward.score;
      user.totalExperience += achievement.reward.experience;
      unlocked.push(achievement);
    }
  }
  
  return unlocked;
}
代码解析

1. 数据流向图

用户选择关卡

获取关卡题目

用户答题

提交答案

计算结果

保存测验记录

更新用户统计

更新关卡状态

检查成就

返回结果给用户

2. 关键数据更新点

步骤 更新内容 影响范围
提交答案 记录答题结果 QuizRecord
结束测验 保存记录、更新统计 User, QuizRecord
关卡更新 更新完成状态和星级 User
成就检查 解锁新成就 User

⚠️ 常见问题与解决方案

问题1: 数据冗余导致不一致

现象:
在多个地方存储相同的数据,导致修改时需要同步更新多处。

错误代码:

// ❌ 错误:统计数据冗余存储
export interface User {
  statistics: UserStatistics;  // 存储统计数据
  // ...
}

// 每次计算都更新统计数据
function updateStatistics(user: User, record: QuizRecord) {
  // 但是 quizHistory 也包含这些数据
  // 导致数据可能不一致
}

正确代码:

// ✅ 正确:统计数据应该实时计算
function getUserStatistics(user: User): UserStatistics {
  const totalQuestions = user.quizHistory.reduce((sum, r) => sum + r.totalQuestions, 0);
  const correctQuestions = user.quizHistory.reduce((sum, r) => sum + r.correctQuestions, 0);
  
  return {
    totalQuestions,
    correctQuestions,
    averageAccuracy: totalQuestions > 0 ? correctQuestions / totalQuestions : 0
  };
}

规则/建议:

  • 避免数据冗余
  • 使用派生数据(从原始数据计算)
  • 或使用单一数据源

问题2: 外键关联错误

现象:
关联查询时使用了错误的外键字段。

错误代码:

// ❌ 错误:使用错误的外键
function getUserAchievements(user: User, achievements: Achievement[]) {
  // 错误:应该使用 achievements 数组中的 id
  return user.quizHistory.filter(h => h.achievements?.includes(h.id));
}

正确代码:

// ✅ 正确:使用正确的外键关联
function getUserAchievements(user: User, achievements: Achievement[]) {
  // 正确:从 achievements 数组中筛选
  return achievements.filter(a => user.achievements.includes(a.id));
}

规则/建议:

  • 明确外键的指向关系
  • 使用正确的字段进行关联
  • 参考 ER 图进行开发

问题3: 循环引用导致死循环

现象:
在数据更新逻辑中出现循环引用,导致死循环。

错误代码:

// ❌ 错误:循环更新
function updateUserStatistics(user: User) {
  // 更新统计数据
  user.statistics.totalQuestions++;
  
  // 触发保存
  saveUser(user);
  
  // saveUser 又调用 updateUserStatistics
}

function saveUser(user: User) {
  // 保存后又更新统计
  updateUserStatistics(user);  // 死循环!
}

正确代码:

// ✅ 正确:分离更新和保存逻辑
function updateUserStatistics(user: User) {
  // 只更新数据,不触发保存
  user.statistics.totalQuestions++;
}

function saveUser(user: User) {
  // 保存数据,不触发更新
  // ...
}

// 在业务逻辑中按顺序调用
function finishQuiz(user: User, record: QuizRecord) {
  updateUserStatistics(user);  // 先更新
  saveUser(user);              // 后保存
}

规则/建议:

  • 避免循环调用
  • 分离关注点
  • 使用单向数据流

问题4: 关系查询性能问题

现象:
在大数据量时,嵌套查询导致性能问题。

错误代码:

// ❌ 错误:嵌套循环查询
function getUserLevelDetails(user: User, levels: Level[]) {
  const details = [];
  
  for (const levelId of user.completedLevels) {
    // 每一次循环都查找关卡
    const level = levels.find(l => l.id === levelId);  // O(n)
    details.push(level);
  }
  
  return details;  // O(m*n)
}

正确代码:

// ✅ 正确:预处理数据
function getUserLevelDetails(user: User, levels: Level[]) {
  // 先转换为 Map,O(n)
  const levelMap = new Map(levels.map(l => [l.id, l]));
  
  // 再查询,O(m)
  return user.completedLevels
    .map(id => levelMap.get(id))
    .filter(level => level !== undefined);
}

规则/建议:

  • 预处理数据,减少查询次数
  • 使用 Map 或 Set 优化查找
  • 避免嵌套循环

问题5: 多对多关系维护困难

现象:
多对多关系的更新逻辑复杂,容易出错。

错误代码:

// ❌ 错误:没有检查就添加
function unlockAchievement(user: User, achievementId: string) {
  // 没有检查是否已存在
  user.achievements.push(achievementId);  // 可能重复
}

正确代码:

// ✅ 正确:检查后再添加
function unlockAchievement(user: User, achievementId: string): boolean {
  // 检查是否已存在
  if (user.achievements.includes(achievementId)) {
    return false;  // 已存在,返回 false
  }
  
  // 添加新成就
  user.achievements.push(achievementId);
  return true;  // 添加成功,返回 true
}

// 在检查成就时使用
function checkAndUnlockAchievements(user: User): Achievement[] {
  const unlocked: Achievement[] = [];
  
  for (const achievement of allAchievements) {
    if (unlockAchievement(user, achievement.id)) {
      unlocked.push(achievement);
    }
  }
  
  return unlocked;
}

规则/建议:

  • 在添加前检查是否已存在
  • 返回操作结果
  • 保持数据唯一性

📝 本章小结

核心知识点

本文详细讲解了数据模型关系图与实体关系设计,主要包括:

1. 数据模型整体架构

  • User、Question、Level、Subject、Achievement、QuizRecord 等模型
  • 各模型的职责和用途

2. 实体关系类型

  • 一对多关系:User → QuizRecord
  • 多对一关系:Question → Level
  • 多对多关系:User ↔ Achievement

3. 数据流向设计

  • 用户测验流程:开始 → 答题 → 结束 → 更新
  • 关键数据更新点

最佳实践总结

外键关联

// 正确:使用正确的外键字段
user.quizHistory.filter(r => r.userId === user.id)

避免数据冗余

// 使用派生数据
const averageAccuracy = correctQuestions / totalQuestions;

优化查询性能

// 使用 Map 预处理
const levelMap = new Map(levels.map(l => [l.id, l]));

多对多关系维护

// 添加前检查
if (!user.achievements.includes(achievementId)) {
  user.achievements.push(achievementId);
}

下一步预告

在下一篇文章中,我们将:

  • 🎨 讲解 UserService 用户服务实现详解
  • 📚 介绍服务层的架构设计
  • 🏷️ 探索单例模式和依赖注入

🔗 相关链接


💡 提示: 建议结合项目源码阅读,动手实践效果更好!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐