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 张表关系图

图:鹿鹿项目 5 张表的 ER 关系——user、archive、handwriting、report、relation

1:N

1:N

1:1

N:M

partner_archive_id

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                      -- 创建时间戳
)

设计要点:

字段类型选择设计理由
idINTEGER PRIMARY KEY AUTOINCREMENT自增主键,保证唯一性和插入效率
open_idTEXT UNIQUE未来对接 HMS 账号时使用,local 表示本地用户
nameTEXT NOT NULL用户昵称,NOT NULL 确保始终有显示名
avatarTEXT(NULL 允许)头像路径,暂无头像时可为空
created_atINTEGER 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
)

设计要点:

字段说明业务价值
sourceTEXT NOT NULL区分数据分析来源(拍照/相册/手写板)
feature_jsonTEXT(JSON)存储 6 维特征向量,避免建立 6 个独立字段
archive_id允许 NULLarchive_id=0 表示"未归类"
created_atINTEGER毫秒时间戳,支持时间的精确排序
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 类型鸿蒙映射使用场景
INTEGERnumber(getLong)主键、时间戳、数值
REALnumber(getDouble)小数(本项目未使用)
TEXTstring(getString)文字、JSON、路径
BLOBArrayBuffer(getBlob)二进制(本项目未使用)

3.2 项目中实际使用的类型

项目类型SQL 类型GetterSetter使用字段
主键 IDINTEGERgetLong自增所有表的 id
文本TEXTgetStringstringname、summary、OCR
时间戳INTEGERgetLongDate.now()created_at、last_record_at
计数INTEGERgetLongnumberrecord_count、word_count
JSONTEXTgetStringJSON.stringify()feature_json、radar_json
枚举TEXTgetStringstring literalsource、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_archiveWHERE archive_id = ? ORDER BY created_at DESCHandwritingDao.listByArchive
idx_report_handwritingWHERE 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: stringNOT NULL,必传字段source TEXT NOT NULL
energy_score?: numberNULL 允许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 张数据库表设计:

  1. user:用户基础信息,open_id 预留 HMS 账号对接
  2. archive:档案分组管理,emoji+color 视觉标识
  3. handwriting:笔迹采集记录,feature_json 存 6 维特征
  4. report:AI 分析报告,radar_json 存雷达分数
  5. relation:双人合盘绑定,通过 partner_archive_id 引用档案

设计原则:规范化为主,适度冗余为辅;明确外键关系,但不由数据库强制;JSON 字段存储结构化但不需要独立查询的复合数据。

下一篇文章将深入 数据库版本升级策略——从 v1 到 v2 的增量迁移实践。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:


思考题: 在你的鸿蒙项目中,是如何处理复杂关联关系(如多对多)的?

  • A. 使用中间关联表(如本项目 relation 表)
  • B. 在字段中存储 JSON 数组(如 partner_ids: string)
  • C. 使用多个独立查询在 TypeScript 层面做联接
  • D. 直接使用原始 SQL JOIN 语句

欢迎在评论区分享你的数据建模经验!

七、设计原则总结

在本项目 5 张表的设计中,遵循了以下核心原则:

原则说明本项目体现
第三范式(3NF)消除传递依赖report 独立于 handwriting
最小化 NULL关键字段设 NOT NULLid、user_id 等主键字段
时间戳标准化统一使用 ISO 8601created_at TEXT 存储字符串
JSON 序列化数组/对象存为 TEXTkeywords、radar_scores 字段
索引策略只为高频查询列建索引user_id、created_at 建索引
外键约束逻辑外键(无 FK 约束)由应用层保证引用完整性

鸿蒙 RDB(SQLite)默认不启用 FOREIGN KEY 约束,需要显式执行 PRAGMA foreign_keys = ON; 才能生效。在本项目中,通过 DAO 层的业务逻辑保证数据的引用完整性。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐