文件库的元数据仓库:ArkTS 为鸿蒙文件管理器设计 UNIQUE 路径字段



实例:文件资料库(FileLib)|技术:UNIQUE 路径去重、元数据字段、类型统计
一、业务需求分析:文件库的数据形态
文件资料库(管理文档/图片/视频/音频/压缩包)是文件元数据管理应用——数据库存「文件的描述信息」(名称、类型、大小、路径、标签、备注),不存文件本体(真实文件在存储系统里)。
核心业务需求:
- 文件记录:名称、类型(文档/图片/视频/音频/压缩包/其他)、大小、路径、标签、备注;
- 路径去重:同一路径的文件不能重复导入——UNIQUE 约束;
- 通配符搜索:按名称或标签模糊搜索——LIKE 通配符;
- 类型筛选与统计:按类型筛选 + 每类型文件数与总大小——GROUP BY 统计;
- 批量导入:一次导入多个文件,已存在的路径跳过。
二、字段设计:文件元数据
文件库表 file_lib
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| name | TEXT | NOT NULL | 文件名 |
| file_type | TEXT | NOT NULL | 类型(文档/图片/视频/音频/压缩包/其他) |
| size | INTEGER | NOT NULL DEFAULT 0 | 大小(字节) |
| path | TEXT | NOT NULL UNIQUE | 唯一路径 |
| tags | TEXT | DEFAULT ‘’ | 逗号分隔标签 |
| note | TEXT | DEFAULT ‘’ | 备注 |
| created_time | INTEGER | NOT NULL | 导入时间戳 |
设计要点拆解:
1. path 用 TEXT + UNIQUE。文件路径(/docs/需求/项目需求文档.docx)是文件在存储系统中的唯一标识——UNIQUE 约束保证同一路径不重复入库。为什么不用 id 去重? id 是自增的(每行不同),文件路径才是业务上的「唯一键」(同一文件导入两次应该是同一份)。业务唯一键用 UNIQUE 约束——这是数据库完整性的核心。
2. size 用 INTEGER 存字节。文件大小以字节为整数存储(120 * 1048576 = 120MB)——存储最小单位,展示时换算(页面 fmtSize 换算 KB/MB/GB)。为什么存字节而不是直接存「120MB」字符串?因为要参与排序与统计(SUM(size) 算总大小)——数值字段才能聚合计算。
3. file_type 用文本枚举。六类:文档/图片/视频/音频/压缩包/其他——参与等值筛选(equalTo('file_type', '文档'))与分组统计(GROUP BY file_type),文本直接用(与影音 type 同理)。
4. tags 用逗号分隔字符串。'需求,产品'——多标签的简化存储(不建标签关联表)。搜索时 LIKE 匹配(LIKE '%需求%')——Demo 级多值方案;真实应用标签多时建「文件-标签」关联表(第 12 实例 CRM 的进阶模式)。
5. 元数据字段(name/type/size/path/tags/note):全部是「描述」——不存文件本体(真实文件在文件系统),数据库只存索引。这是文件管理器类应用的通用架构:DB 存元数据,文件系统存本体。
三、建表 SQL
CREATE TABLE IF NOT EXISTS file_lib (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
file_type TEXT NOT NULL,
size INTEGER NOT NULL DEFAULT 0,
path TEXT NOT NULL UNIQUE,
tags TEXT DEFAULT '',
note TEXT DEFAULT '',
created_time INTEGER NOT NULL
);
path TEXT NOT NULL UNIQUE——唯一约束的核心体现:插入重复路径 → SQLite 抛约束错误(批量导入的「跳过逻辑」在应用层处理,见 16-3)。UNIQUE 是「路径去重」的数据库级保障——即使应用层忘了判断,数据库也不会产生重复路径。
四、FileLibDao 封装:搜索与统计是亮点
数据层核心 FileLibDao,本实例的独特方法是search(LIKE 通配符搜索) 与 typeStats(类型分组统计):
按类型筛选:
static async queryByType(context: common.Context, fileType: string): Promise<FileRecord[]> {
const store = await FileLibDao.getStore(context);
const predicates = new relationalStore.RdbPredicates(FileLibDao.TABLE);
if (fileType !== '全部') {
predicates.equalTo('file_type', fileType);
}
predicates.orderByDesc('created_time');
const result = await store.query(predicates);
return FileLibDao.collect(result);
}
‘全部’ 哨兵值:fileType === '全部' 时不过滤(查全表)——筛选条的「全部」选项在 DAO 层用哨兵值跳过谓词。「全部」不在数据库层处理,在查询层跳过。
通配符搜索(LIKE):
static async search(context: common.Context, keyword: string): Promise<FileRecord[]> {
const store = await FileLibDao.getStore(context);
const predicates = new relationalStore.RdbPredicates(FileLibDao.TABLE);
predicates.like('name', `%${keyword}%`)
.or()
.like('tags', `%${keyword}%`)
.orderByDesc('created_time');
const result = await store.query(predicates);
return FileLibDao.collect(result);
}
生成的 SQL:
SELECT * FROM file_lib
WHERE name LIKE '%需求%' OR tags LIKE '%需求%'
ORDER BY created_time DESC;
LIKE 通配符:%关键字% 匹配「包含关键字」——%需求% 匹配「项目需求文档.docx」与 tags 含「需求」的文件。% 是任意字符通配符(0 个或多个),_ 是单字符通配符。LIKE 的 OR 组合:名称或标签任一命中即返回——搜索的「多字段命中」。
类型统计(GROUP BY + COUNT + SUM):
static async typeStats(context: common.Context): Promise<TypeStat[]> {
const store = await FileLibDao.getStore(context);
const result = await store.querySql(
`SELECT file_type, COUNT(*) AS cnt, SUM(size) AS total_size
FROM ${FileLibDao.TABLE}
GROUP BY file_type
ORDER BY cnt DESC`
);
const list: TypeStat[] = [];
while (result.goToNextRow()) {
const item: TypeStat = {
fileType: result.getString(result.getColumnIndex('file_type')),
count: result.getLong(result.getColumnIndex('cnt')),
totalSize: result.getLong(result.getColumnIndex('total_size')),
};
list.push(item);
}
result.close();
return list;
}
GROUP BY + COUNT + SUM 三合一:每个类型一行——类型名 + 文件数(COUNT)+ 总大小(SUM)。ORDER BY cnt DESC:文件数多的类型排前(文档通常最多)。TypeStat 接口:{ fileType, count, totalSize }——统计结果的结构化返回。
总统计:
static async totalStats(context: common.Context): Promise<TotalStat> {
// SELECT COUNT(*) AS cnt, SUM(size) AS total_size FROM file_lib
}
标题栏「18 个文件 · 共 1.7GB」——COUNT + SUM 无分组(全表统计)。TotalStat 接口:{ count, totalSize }。
五、技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 路径去重 | path UNIQUE 约束 | 防重复导入 |
| 模糊搜索 | LIKE ‘%kw%’ OR LIKE | 名称/标签命中 |
| 类型统计 | GROUP BY + COUNT + SUM | 每类文件数/大小 |
| 全表统计 | COUNT + SUM 无分组 | 标题栏数字 |
| 批量导入 | 路径先查后插 | 去重导入 |
| 大小单位 | 字节存储 + fmtSize 换算 | 可聚合可展示 |
六、文章小结
文件资料库的数据层核心是**「UNIQUE 路径去重 + LIKE 搜索 + GROUP BY 统计」**:path UNIQUE 约束从数据库层保证文件路径唯一(防重复导入),LIKE 通配符实现名称/标签的多字段模糊搜索,GROUP BY + COUNT + SUM 三合一给出每类型文件数与总大小(类型筛选条的动态来源)。字节整数存储让大小可聚合(SUM)可换算(展示)。这是「文件元数据管理类应用」的通用数据模型。
下一篇(16-2)展示文件浏览器 UI——搜索框 + 动态类型筛选条 + 图标行列表。
七、文件元数据字段设计详解
文件库 7 个字段各司其职,其中 path、size、file_type、tags 四个字段的设计决策最值得展开——它们分别对应「去重、聚合、筛选、搜索」四类核心能力。
7.1 path UNIQUE:路径是业务唯一键
| 对比项 | id 主键 | path UNIQUE |
|---|---|---|
| 生成方式 | 自增(数据库自动) | 文件系统决定(业务产生) |
| 语义 | 行标识,与业务无关 | 文件的真实身份(位置) |
| 重复导入 | id 不同,无法拦截 | 约束报错,天然拦截 |
| 结论 | 仅作内部主键 | 业务唯一键必须 UNIQUE |
关键认知:UNIQUE 约束是数据库级的最后防线。批量导入时应用层「先查再插」只是第一道防线(省一次插入开销);即使应用层判断逻辑写错,path UNIQUE 也会让 SQLite 抛约束错误,杜绝脏数据。约束在数据库,防御分两层:
// 应用层:先查重(第一道防线)
const exist = await FileLibDao.queryByPath(context, path);
if (exist) { return; } // 已存在则跳过
// 数据库层:UNIQUE 兜底(第二道防线)
// 若上面判断漏了,INSERT 会抛 SQLITE_CONSTRAINT_UNIQUE
7.2 size 字节存储:整数才能聚合
- 存字节(INTEGER):
2.3MB = 2.3 * 1048576的整数——可 SUM、可排序、可比较; - 存「2.3MB」字符串:SUM 无意义、排序错乱(‘120MB’ 按字典序排在 ‘25MB’ 前面);
- 展示换算(fmtSize):读取时按 1024 分级换算 KB/MB/GB——存储与展示分离。
7.3 file_type 文本枚举
六类文本枚举(文档/图片/视频/音频/压缩包/其他)直接参与等值筛选(equalTo('file_type', '图片'))与分组统计(GROUP BY file_type)。文本在此场景与数值效率无差异(六类基数极小),且可读性最好——SQL 结果直接显示,无需再映射成中文。
7.4 tags 逗号分隔
'需求,产品' 是多值属性的简化建模——一个字段存多个标签。代价:无法做「按标签精确统计」,搜索只能 LIKE 模糊命中。Demo 级取舍:文件数少(18 个)时 LIKE 扫描全表毫无压力;标签多、要按标签统计时,升级为「文件-标签」关联表(CRM 实例的进阶模式)。
八、元数据与文件本体的分离架构
┌───────────────┐ path ┌──────────────────┐
│ file_lib 表 │ ────────────► │ 文件系统/相册 │
│ 存「描述」 │ │ 存「本体」 │
│ name/size/tags │ │ /docs/需求/xx.docx │
└───────────────┘ └──────────────────┘
DB 索引 真实数据
为什么分离?
- DB 不存大对象:文件本体可能几百 MB(本实例种子最大 800MB),入库会让数据库膨胀、备份变慢;
- 文件系统已是最好的存储:读写、流式播放、权限管理都是系统级能力;
- 元数据可高速查询:文件数统计、类型聚合、模糊搜索都在 DB 索引层面完成,不用打开文件;
- 删除语义清晰:删记录 ≠ 删文件(只清索引);「彻底删除」才联动文件系统——两个动作由应用层编排。
鸿蒙侧实现:元数据在 file_lib 表,本体在沙箱路径指向的真实文件。UI 点击行时用 path 打开/分享文件,DB 只管描述——path 是连接两层的桥梁字段:
interface FileRecord {
id: number;
name: string; // 展示用
path: string; // 定位本体用(桥梁)
size: number; // 统计用(字节)
fileType: string; // 筛选/分组用
tags: string; // 搜索用
note: string;
createdTime: number;
}
九、字节存储的可聚合性
类型统计的核心 SQL 依赖 size 是数值:
SELECT file_type, COUNT(*) AS cnt, SUM(size) AS total_size
FROM file_lib GROUP BY file_type ORDER BY cnt DESC;
| 存储类型 | COUNT | SUM | AVG | 排序 |
|---|---|---|---|---|
| INTEGER 字节 | ✅ | ✅ | ✅ | ✅ 数值序 |
| TEXT “120MB” | ✅ | ❌ | ❌ | ❌ 字典序 |
一个字段的存储类型决定它的能力边界——选 INTEGER 存字节,等于同时解锁「总大小(SUM)」「每类平均大小(AVG)」「按大小排序(ORDER BY size DESC)」三个能力。展示侧 fmtSize 是纯函数:
function fmtSize(size: number): string {
if (size >= 1024 * 1024 * 1024) return (size / (1024 ** 3)).toFixed(1) + 'GB';
if (size >= 1024 * 1024) return (size / (1024 ** 2)).toFixed(1) + 'MB';
if (size >= 1024) return (size / 1024).toFixed(0) + 'KB';
return size + 'B';
}
存储不做加工,展示才换算——数据库永远存原始字节,单位换算只发生在渲染层。
十、LIKE 搜索思路预览
后续文章(16-2/16-3)将展开完整搜索实现,这里先给出建表层就要想好的搜索设计:
-- 名称或标签命中关键字
SELECT * FROM file_lib
WHERE name LIKE '%需求%' OR tags LIKE '%需求%'
ORDER BY created_time DESC;
三个提前设计:
- name 与 tags 都是 TEXT——LIKE 可直接作用,无需转换;
- tags 的逗号分隔格式决定了搜索是「包含匹配」而非「等值匹配」——建表时就想清楚了;
- 中文 LIKE 无需转义,英文如需忽略大小写可配合
COLLATE NOCASE。
十一、FAQ
Q1:path 用 UNIQUE,重复导入时会发生什么?
A:SQLite 抛 SQLITE_CONSTRAINT_UNIQUE 约束错误。应用层两条路:插入前先查重(省开销),或 try/catch 捕获约束错误后跳过该条——查重省开销,UNIQUE 兜底。
Q2:size 为什么不直接存「2.3MB」这种人类可读字符串?
A:字符串无法 SUM/排序/比较(‘120MB’ 字典序小于 ‘25MB’)。字节整数存储后由展示层按 1024 换算——存储与展示解耦,统计能力完整。
Q3:tags 用逗号分隔,会不会有歧义?
A:约定标签本身不含逗号(导入时清洗/替换)即可。这是「一列多值」的简化方案,查询用 LIKE 包含匹配;需要精确按标签筛选时可用 LIKE '%,需求,%' 加逗号边界。
Q4:UNIQUE 和 PRIMARY KEY 都是唯一,为什么还要 path UNIQUE?
A:PRIMARY KEY 约束主键(id,自增),语义是「行标识」;UNIQUE 约束业务键(path),语义是「业务上不能重复」——主键管内部,UNIQUE 管业务,两者各司其职。
Q5:文件删除了,数据库记录还在怎么办?
A:文件库只管理元数据,删记录只清 DB 索引;若要求同步清理本体,应用层在删除时用 path 调文件系统删除接口——删记录与删文件两个动作要在同一事务语义里编排(先删 DB 成功再删文件,或反之)。
更多推荐

所有评论(0)