HarmonyOS应用<趣答>开发第4篇:数据模型关系图与实体关系设计——构建完整的数据架构
·

📖 引言
在知识问答学习应用中,数据模型之间的关系设计是构建整个应用架构的基础。合理的数据模型关系不仅能够提高数据的组织效率,还能简化业务逻辑的实现,让代码更加清晰和可维护。
本文将详细讲解各个数据模型之间的关系设计,包括一对多、多对一、多对多等关系类型,以及数据流向和业务逻辑的组织。通过本文,你将掌握:
- 如何设计和理解数据模型之间的关系
- 如何使用 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 语法绘制实体关系图,展示各个模型之间的关系。
完整代码
代码解析
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 用户服务实现详解
- 📚 介绍服务层的架构设计
- 🏷️ 探索单例模式和依赖注入
🔗 相关链接
- 项目源码: Atomgit仓库
💡 提示: 建议结合项目源码阅读,动手实践效果更好!
更多推荐


所有评论(0)