25 数据库设计与表结构规划:5 张表的设计思路
25 数据库设计与表结构规划:5 张表的设计思路
前言

图:25 数据库设计与表结构规划:5 张表的设计思路 运行效果截图(HarmonyOS NEXT)
数据库表结构的设计直接关系到应用的数据组织方式、查询效率和扩展性。一个良好的表结构设计应该满足当前需求、预留扩展空间、并保持查询效率。
本文以"鹿鹿·笔迹心理分析"项目中的 5 张核心表为例,详细讲解鸿蒙 RDB 数据库的表结构设计思路、字段类型选择、约束策略和索引优化方案。
鸿蒙官方·RDB 建表语法:developer.huawei.com
[Models.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/Models.ets) — 实体类型定义

图:鹿鹿项目 5 张表的 ER 关系——user、archive、handwriting、report、relation
一、数据模型概览
1.1 5 张表的关系
user ──1:N──→ archive ──1:N──→ handwriting ──1:1──→ report
│ │
│ └── relation(partner_archive_id)
└──────── relation(user_id)
| 表名 | 中文名 | 核心作用 | 数据量级 | 外键关联 |
|---|---|---|---|---|
user |
用户 | 本地用户信息 | ~1 条 | 关联 archive.user_id |
archive |
档案 | 按人分组管理 | 5-20 条 | 关联 user.id |
handwriting |
笔迹 | 每次采集的记录 | 数百条 | 关联 archive.id |
report |
报告 | AI 分析结果 | 同 handwriting | 关联 handwriting.id |
relation |
关系 | 双人合盘绑定 | ~1 条 | 关联 user.id |
1.2 DDL 语句
// DatabaseService.ets — upgrade 方法中的建表 SQL
const ddls: string[] = [
`CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
open_id TEXT UNIQUE,
name TEXT NOT NULL,
avatar TEXT,
created_at INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS archive (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
emoji TEXT,
relation TEXT,
color TEXT,
last_record_at INTEGER,
record_count INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS handwriting (
id INTEGER PRIMARY KEY AUTOINCREMENT,
archive_id INTEGER,
source TEXT NOT NULL,
image_path TEXT,
ocr_text TEXT,
feature_json TEXT,
energy_score INTEGER,
device_type TEXT DEFAULT '',
word_count INTEGER DEFAULT 0,
write_duration INTEGER DEFAULT 0,
created_at INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS report (
id INTEGER PRIMARY KEY AUTOINCREMENT,
handwriting_id INTEGER NOT NULL,
archive_id INTEGER,
personality_type TEXT,
summary TEXT,
radar_json TEXT,
tags_json TEXT,
created_at INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS relation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
partner_archive_id INTEGER NOT NULL,
bound_method TEXT,
match_score INTEGER,
created_at INTEGER NOT NULL
)`
]
二、表结构详解
2.1 user(用户表)
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键,自增
open_id TEXT UNIQUE, -- 用户标识(HMS/本地)
name TEXT NOT NULL, -- 用户名
avatar TEXT, -- 头像路径
created_at INTEGER NOT NULL -- 创建时间戳
)
设计要点:
| 字段 | 类型选择 | 设计理由 |
|---|---|---|
id |
INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键,保证唯一性和插入效率 |
open_id |
TEXT UNIQUE | 未来对接 HMS 账号时使用,local 表示本地用户 |
name |
TEXT NOT NULL | 用户昵称,NOT NULL 确保始终有显示名 |
avatar |
TEXT(NULL 允许) | 头像路径,暂无头像时可为空 |
created_at |
INTEGER NOT NULL | 时间戳以毫秒为单位(13 位),not null |
2.2 archive(档案表)
CREATE TABLE IF NOT EXISTS archive (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, -- 关联 user.id
name TEXT NOT NULL, -- 档案名称
emoji TEXT, -- 档案图标(emoji 字符串)
relation TEXT, -- 关系标签("伴侣"/"未设置")
color TEXT, -- 主题色(hex 字符串)
last_record_at INTEGER, -- 最近记录时间
record_count INTEGER NOT NULL DEFAULT 0, -- 记录总数
created_at INTEGER NOT NULL
)
设计要点:
| 字段 | 作用 | 业务映射 |
|---|---|---|
user_id |
归属用户 | 一个用户可有多个档案 |
relation |
关系分类 | '伴侣' 的档案收入"合盘"模块 |
emoji + color |
视觉标识 | 档案卡片上的图标和背景色 |
last_record_at |
排序依据 | 首页按最近记录时间排序 |
record_count |
展示计数 | 冗余字段,避免每次都 COUNT 查询 |
2.3 handwriting(笔迹表)
CREATE TABLE IF NOT EXISTS handwriting (
id INTEGER PRIMARY KEY AUTOINCREMENT,
archive_id INTEGER, -- 档案(NULL=未归类)
source TEXT NOT NULL, -- 来源:camera/gallery/handwrite/demo
image_path TEXT, -- 图片路径
ocr_text TEXT, -- OCR 识别文本
feature_json TEXT, -- JSON:6 维特征向量
energy_score INTEGER, -- 能量值(0-100)
device_type TEXT DEFAULT '', -- 设备类型(v2 新增)
word_count INTEGER DEFAULT 0, -- 字数统计(v2 新增)
write_duration INTEGER DEFAULT 0, -- 书写时长 ms(v2 新增)
created_at INTEGER NOT NULL
)
设计要点:
| 字段 | 说明 | 业务价值 |
|---|---|---|
source |
TEXT NOT NULL | 区分数据分析来源(拍照/相册/手写板) |
feature_json |
TEXT(JSON) | 存储 6 维特征向量,避免建立 6 个独立字段 |
archive_id |
允许 NULL | archive_id=0 表示"未归类" |
created_at |
INTEGER | 毫秒时间戳,支持时间的精确排序 |
v2 新增字段 |
含默认值 | 向前兼容的字段扩展示例 |
2.4 report(报告表)
CREATE TABLE IF NOT EXISTS report (
id INTEGER PRIMARY KEY AUTOINCREMENT,
handwriting_id INTEGER NOT NULL, -- 关联 handwriting.id
archive_id INTEGER, -- 冗余,便于按档案查询
personality_type TEXT, -- 人格类型(如"温和理性型")
summary TEXT, -- 一句话总结
radar_json TEXT, -- JSON: 6 维雷达分数
tags_json TEXT, -- JSON: 标签数组
created_at INTEGER NOT NULL
)
JSON 字段的设计理由:
// 如果不用 JSON 字段,雷达图需要 6 列
// ❌ 方案 A:展开为 6 个独立字段
energy REAL, thinking REAL, action REAL,
emotion REAL, social REAL, detail REAL
// ✅ 方案 B:JSON 字符串(本项目采用)
radar_json TEXT -- '{"energy":62,"thinking":68,"action":55,"emotion":71,"social":60,"detail":75}'
JSON 字段的优势:
| 对比项 | 6 个独立字段 | JSON 字段 |
|---|---|---|
| 列数 | +6 列 | +1 列 |
| 扩展性 | 新增维度需 ALTER TABLE | 直接修改 JSON 结构 |
| 代码量 | 6 个 getter/setter | 序列化/反序列化一次 |
| 查询 | 可 WHERE 过滤某个维度 | 无法直接过滤 |
| 适用场景 | 需要按维度筛选 | 仅展示,不需要筛选 |
2.5 relation(关系表)
CREATE TABLE IF NOT EXISTS relation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, -- 用户
partner_archive_id INTEGER NOT NULL, -- 伴侣的档案 ID
bound_method TEXT, -- 绑定方式:tap/qrcode/phone
match_score INTEGER, -- 匹配度 0-100
created_at INTEGER NOT NULL
)
设计要点:
- 通过
partner_archive_id关联实际伴侣的档案数据 bound_method记录绑定方式(NFC 碰一碰/二维码/手机号)match_score缓存匹配度计算结果
提示:relation 表设计的核心思路是"用一个外键引用 partner 的档案,而非引用 partner 的用户表"。这样可以直接复用 archive 中的档案数据。
三、字段类型对照
3.1 鸿蒙 RDB 支持的类型
| SQLite 类型 | 鸿蒙映射 | 使用场景 |
|---|---|---|
INTEGER |
number(getLong) |
主键、时间戳、数值 |
REAL |
number(getDouble) |
小数(本项目未使用) |
TEXT |
string(getString) |
文字、JSON、路径 |
BLOB |
ArrayBuffer(getBlob) |
二进制(本项目未使用) |
3.2 项目中实际使用的类型
| 项目类型 | SQL 类型 | Getter | Setter | 使用字段 |
|---|---|---|---|---|
| 主键 ID | INTEGER | getLong |
自增 | 所有表的 id |
| 文本 | TEXT | getString |
string |
name、summary、OCR |
| 时间戳 | INTEGER | getLong |
Date.now() |
created_at、last_record_at |
| 计数 | INTEGER | getLong |
number |
record_count、word_count |
| JSON | TEXT | getString |
JSON.stringify() |
feature_json、radar_json |
| 枚举 | TEXT | getString |
string literal |
source、bound_method |
四、索引设计
4.1 项目中的索引
CREATE INDEX IF NOT EXISTS idx_handwriting_archive
ON handwriting(archive_id, created_at DESC) -- 组合索引,支持按档案+时间查询
CREATE INDEX IF NOT EXISTS idx_report_handwriting
ON report(handwriting_id) -- 单列索引,支持笔迹→报告查询
4.2 索引覆盖的查询场景
| 索引名称 | 加速的查询 | 涉及方法 |
|---|---|---|
idx_handwriting_archive |
WHERE archive_id = ? ORDER BY created_at DESC |
HandwritingDao.listByArchive |
idx_report_handwriting |
WHERE handwriting_id = ? |
ReportDao.findByHandwriting |
4.3 索引设计原则
- 为高频 WHERE 字段建索引:archive_id、handwriting_id 频繁出现在查询条件中
- 为 ORDER BY 字段建索引:created_at DESC 是最常用的排序方向
- 避免过量索引:5 张表只建 2 个索引,因为大部分查询在小数据量(<500 条)下走全表扫描也很快
- 组合索引的列顺序:将区分度高的列放在前面(archive_id 比 created_at 更数据集中)
五、实体类型定义
5.1 接口定义
// Models.ets
export interface HandwritingEntity {
id?: number
archive_id?: number
source: string // 'camera' | 'gallery' | 'handwrite'
image_path?: string
ocr_text?: string
feature_json?: string // 6 维特征向量
energy_score?: number
device_type?: string
word_count?: number
write_duration?: number
created_at: number
}
5.2 可选字段的语义
| 写法 | 说明 | 对应 SQL |
|---|---|---|
id?: number |
主键用于读取时赋值 | PRIMARY KEY AUTOINCREMENT |
archive_id?: number |
可为 NULL,未归类 | archive_id INTEGER |
source: string |
NOT NULL,必传字段 | source TEXT NOT NULL |
energy_score?: number |
NULL 允许 | energy_score INTEGER |
提示:Entity 接口中的可选字段
?对应 SQL 中的 NULL 允许字段。NOT NULL 的字段在接口中写为field: type。
六、表结构设计总结
6.1 设计检查清单
| 检查项 | 项目中示例 | 是否满足 |
|---|---|---|
| 每张表都有主键 | ✅ 所有表有 id INTEGER PRIMARY KEY AUTOINCREMENT |
✅ |
| 外键关系明确 | ✅ user→archive→handwriting→report | ✅ |
| 冗余字段合理 | ✅ archive.record_count(避免 COUNT 查询) | ✅ |
| 时间戳统一 | ✅ 所有 created_at 为 INTEGER(毫秒) | ✅ |
| JSON 字段适度 | ✅ 仅在 feature_json、radar_json、tags_json 使用 | ✅ |
| 索引覆盖高频查询 | ✅ 2 个索引覆盖所有关联查询 | ✅ |
| 预留扩展 | ✅ 字段有默认值,v2 可新增列 | ✅ |
6.2 不采用的方案
| 方案 | 原因 | 替代选择 |
|---|---|---|
| 使用自增 UUID 做主键 | INTEGER 更高效、更易读 | INTEGER PRIMARY KEY |
| 雷达成 6 维独立列 | 扩展性差 | JSON 列 |
| storage 存储附件图片 | 图片通过文件系统管理 | 数据库中只存 image_path |
| 使用外键约束 | 鸿蒙 RDB 不强制外键 | 应用层保证一致性 |
八、注意事项与常见问题
8.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期:合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
总结
本文详细解析了"鹿鹿"项目的 5 张数据库表设计:
- user:用户基础信息,
open_id预留 HMS 账号对接 - archive:档案分组管理,emoji+color 视觉标识
- handwriting:笔迹采集记录,
feature_json存 6 维特征 - report:AI 分析报告,
radar_json存雷达分数 - relation:双人合盘绑定,通过
partner_archive_id引用档案
设计原则:规范化为主,适度冗余为辅;明确外键关系,但不由数据库强制;JSON 字段存储结构化但不需要独立查询的复合数据。
下一篇文章将深入 数据库版本升级策略——从 v1 到 v2 的增量迁移实践。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- 鸿蒙 RDB 建表语法
- [Models.ets 实体定义](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/Models.ets)
- [DatabaseService.ets 建表源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/DatabaseService.ets)
- SQLite CREATE TABLE 语法
- CREATE INDEX 索引优化
- JSON 字段序列化
- 鸿蒙开发文档
- 数据持久化指南
- SQLite 范式设计
- 鸿蒙 FOREIGN KEY 说明
思考题: 在你的鸿蒙项目中,是如何处理复杂关联关系(如多对多)的?
- A. 使用中间关联表(如本项目 relation 表)
- B. 在字段中存储 JSON 数组(如
partner_ids: string)- C. 使用多个独立查询在 TypeScript 层面做联接
- D. 直接使用原始 SQL JOIN 语句
欢迎在评论区分享你的数据建模经验!
七、设计原则总结
在本项目 5 张表的设计中,遵循了以下核心原则:
| 原则 | 说明 | 本项目体现 |
|---|---|---|
| 第三范式(3NF) | 消除传递依赖 | report 独立于 handwriting |
| 最小化 NULL | 关键字段设 NOT NULL | id、user_id 等主键字段 |
| 时间戳标准化 | 统一使用 ISO 8601 | created_at TEXT 存储字符串 |
| JSON 序列化 | 数组/对象存为 TEXT | keywords、radar_scores 字段 |
| 索引策略 | 只为高频查询列建索引 | user_id、created_at 建索引 |
| 外键约束 | 逻辑外键(无 FK 约束) | 由应用层保证引用完整性 |
鸿蒙 RDB(SQLite)默认不启用 FOREIGN KEY 约束,需要显式执行
PRAGMA foreign_keys = ON;才能生效。在本项目中,通过 DAO 层的业务逻辑保证数据的引用完整性。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)