ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据的身家性命:ArkTS 为鸿蒙备份导出设计目标库结构

数据的身家性命:ArkTS 为鸿蒙备份导出设计目标库结构


实例:数据备份导出(Backup)|技术:备份目标库设计、BackupDao 封装

一、业务需求分析:备份导出的本质

数据是应用的「身家性命」——用户记的账、写的日记、存的联系人,丢了就是灾难。备份导出(Backup/Restore)是数据治理的核心能力,业务需求拆解:

  1. 导出:把数据源表的数据序列化成 JSON / CSV 文本,写入应用沙箱文件;
  2. 记录备份:每次导出生成一条备份日志(文件名、大小、时间、格式),可追溯;
  3. 恢复导入:读取备份内容,清空数据源表后重新导入(事务保证原子);
  4. 删除备份:删除备份记录 + 沙箱文件(双删除);
  5. 终端化体验:页面用「终端控制台」风格展示操作日志——像黑客帝国一样酷。

技术栈:本实例首次引入文件读写(@kit.CoreFileKitfileIo)——数据库能力 + 文件系统的结合,让「数据离开数据库变成文件,再从文件回到数据库」的闭环成立。

二、双表结构设计:备份日志表 + 数据源表

备份日志表 backup_log

字段名类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT自增主键
file_nameTEXTNOT NULL备份文件名(backup_时间戳.json)
sizeINTEGERNOT NULL DEFAULT 0文件大小(字节)
source_tableTEXTNOT NULL数据源表名
backup_timeINTEGERNOT NULL备份时间戳
typeTEXTNOT NULL DEFAULT ‘json’格式:json / csv

数据源表 backup_source

字段名类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT自增主键
nameTEXTNOT NULL条目名称
categoryTEXTDEFAULT ‘’分类
amountREALNOT NULL DEFAULT 0金额
noteTEXTDEFAULT ‘’备注
created_timeINTEGERNOT NULL创建时间戳

设计要点拆解

1. backup_log 是「备份的元数据」。它不存备份内容(内容在沙箱文件里),只存「哪次备份、什么文件、多大、何时」——是文件系统的索引表。元数据与内容分离:内容在文件(可大可小),索引在数据库(查询快)。

2. backup_source 是「被备份的业务数据」。模拟一个真实的业务表(比如「资产清单」:华为手机、蓝牙耳机、运动鞋…),12 条数据用于导出/恢复演示。实际项目中它可以是任何业务表(联系人、账单、日记)——本实例用它代表「任意可备份的数据源」。

3. type 区分格式。json / csv 两种导出格式,backup_time 时间戳排序备份历史。

4. source_table 字段的扩展意义。记录「备份的是哪张表」——如果未来备份多张表(联系人表 + 账单表),这个字段让日志可区分来源。为多表备份预留是设计前瞻。

三、建表 SQL

CREATETABLEIFNOTEXISTSbackup_log(idINTEGERPRIMARYKEYAUTOINCREMENT,file_nameTEXTNOTNULL,sizeINTEGERNOTNULLDEFAULT0,source_tableTEXTNOTNULL,backup_timeINTEGERNOTNULL,typeTEXTNOTNULLDEFAULT'json');CREATETABLEIFNOTEXISTSbackup_source(idINTEGERPRIMARYKEYAUTOINCREMENT,nameTEXTNOTNULL,categoryTEXTDEFAULT'',amountREALNOTNULLDEFAULT0,noteTEXTDEFAULT'',created_timeINTEGERNOTNULL);

两张表都无需额外索引(backup_log 按时间倒序查询可接受全表扫描,数据量小;backup_source 全量读取)。

四、BackupDao 封装:从数据库到文件的桥梁

数据层核心BackupDao,本实例第一次引入fileIo(文件 IO):

import{fileIo}from'@kit.CoreFileKit';

实体接口(导出结构):

exportinterfaceBackupRecord{id:number;fileName:string;size:number;sourceTable:string;backupTime:number;type:string;}exportinterfaceSourceRecord{id:number;name:string;category:string;amount:number;note:string;createdTime:number;}

BackupRecord对应 backup_log 表;SourceRecord对应 backup_source 表(也是 JSON 导出的行结构)。

五、数据源查询:导出前的读取

导出第一步是从数据源表读出全部记录:

staticasyncquerySource(context:common.Context):Promise<SourceRecord[]>{conststore=awaitBackupDao.getStore(context);constresult=awaitstore.querySql(`SELECT * FROM${BackupDao.SOURCE_TABLE}ORDER BY created_time DESC`);constlist:SourceRecord[]=[];while(result.goToNextRow()){constr:SourceRecord={id:result.getLong(result.getColumnIndex('id')),name:result.getString(result.getColumnIndex('name')),category:result.getString(result.getColumnIndex('category'))||'',amount:result.getDouble(result.getColumnIndex('amount')),note:result.getString(result.getColumnIndex('note'))||'',createdTime:result.getLong(result.getColumnIndex('created_time')),};list.push(r);}result.close();returnlist;}

查询结果就是「待序列化的数据」——下一步把它转成 JSON / CSV 字符串。

六、序列化:JSON 与 CSV 的生成

JSON 导出——JSON.stringify一把梭:

staticasyncexportJson(context:common.Context):Promise<string>{constrows=awaitBackupDao.querySource(context);returnJSON.stringify(rows);}

JSON.stringify(rows)的产出[{"id":1,"name":"华为 Mate 60 Pro","category":"数码","amount":6999,...},...]——数组序列化为 JSON 文本,可读、可恢复。

CSV 导出——手工拼接(带表头):

staticasyncexportCsv(context:common.Context):Promise<string>{constrows=awaitBackupDao.querySource(context);letcsv='id,name,category,amount,note,created_time\n';for(constrofrows){csv+=`${r.id},${r.name},${r.category},${r.amount},${r.note},${r.createdTime}\n`;}returncsv;}

CSV 格式:首行表头(id,name,…),每行一条记录,逗号分隔。CSV 的优势是可以被 Excel 直接打开——用户导出后双击即看,这是比 JSON 更「用户友好」的格式。两种格式满足两种需求:JSON 给程序(可恢复),CSV 给人(可查看)。

CSV 的潜在坑:如果字段值本身含逗号或换行(如备注「a,b」),简单拼接会破坏列结构。本实例数据不含逗号,直接拼接够用;生产环境应对字段做引号包裹("${value}")转义。

七、文件写入:fileIo 的打开-写入-关闭

序列化后的文本要落盘到应用沙箱:

staticasyncwriteFile(context:common.Context,fileName:string,content:string):Promise<FileWriteResult>{constfilesDir=context.filesDir;// 应用沙箱文件目录constpath=`${filesDir}/${fileName}`;constfile=fileIo.openSync(path,fileIo.OpenMode.READ_WRITE|fileIo.OpenMode.CREATE|fileIo.OpenMode.TRUNC);fileIo.writeSync(file.fd,content);fileIo.closeSync(file);conststat=fileIo.statSync(path);constr:FileWriteResult={path:path,size:stat.size};returnr;}

fileIo 三件套

调用作用
openSync(path, mode)打开文件,返回 file 对象(含 fd 文件描述符)
writeSync(fd, content)写入内容
closeSync(file)关闭文件(必须!释放句柄)

OpenMode 组合READ_WRITE | CREATE | TRUNC——可读写、不存在则创建、已存在则截断清空。TRUNC 保证每次写入都是「全新文件」而非追加残留。

context.filesDir:应用沙箱的文件目录(如/data/app/el2/100/base/com.example.xiangcejihe/haps/entry/files)——每个应用独立的私有目录,写文件不需要任何权限(沙箱内自由读写)。沙箱文件是应用私有数据,其他应用无法访问,安全有保障。

statSync(path).size:写完后取文件大小(字节)——用于 backup_log 的 size 字段,日志记录「这次备份多大」。

八、技术要点对照表

技术点实现方式生产价值
元数据分离backup_log 索引 + 沙箱文件内容日志查询快
JSON 导出JSON.stringify(rows)程序可恢复
CSV 导出表头 + 行拼接Excel 可打开
文件写入openSync/writeSync/closeSync沙箱落盘
OpenModeREAD_WRITE|CREATE|TRUNC截断重写
沙箱目录context.filesDir免权限私有存储

九、文章小结

备份实例建立了**「数据库 + 文件」双栖架构**:数据源表存业务数据,序列化(JSON/CSV)导出到沙箱文件,backup_log 记录备份元数据。技术新能力是 fileIo 文件读写(open/write/close 三件套 + OpenMode 组合 + filesDir 沙箱),与数据库操作组合成「导出 → 落盘 → 记录」的完整链路。下一篇(10-3)会讲完整的备份执行与恢复导入,那是本实例的操作核心。

动手练习:运行 App 进入备份页,点「导出 JSON」,然后用 DevEco Studio 的 Device File Explorer 找到沙箱目录下的 backup_xxx.json,双击打开查看导出的内容结构。

十、备份记录表字段设计详解:四个字段撑起一次备份的档案

backup_log 记录的不是备份内容,而是「一次备份的档案」。逐字段拆解设计意图:

字段存什么为什么这么设计使用场景
file_namebackup_1752xxx.json时间戳命名天然唯一,无需 UUID列表展示、删除时定位文件
source_tablebackup_source记录「这次备份的是哪张表」多表备份时区分日志归属
size1248(字节)文件大小,列表展示「占多大空间」排序、容量感知
backup_time1752xxx(ms)时间戳而非字符串,可直接排序比较按时间倒序展示历史

两个设计取舍

1. 不落库 file_path。备份文件路径可由filesDir + '/' + file_name推导,存了反而冗余——目录变了会留下脏数据。可推导的字段不落库是反范式设计的一条实用原则。

2. backup_time 用 INTEGER 不用 TEXT'2025-07-01 10:00'字符串比较需要格式化一致才能排序;时间戳数字天然可比,页面展示时再formatDate(ts)转字符串。存储用机器格式,展示用人话格式

备份名的可读性扩展:若希望文件名更友好,可改为backup_20250701_1000.json(时间戳格式化拼接),字段设计不变,只是命名规则变。

十一、多表备份:数据源范围的设计与遍历

当前实例只备份 backup_source 一张表,但字段source_table已为多表预留。多表备份的设计:

方案 A:备份配置表——维护一张backup_scope表登记「可备份的表清单」:

字段名类型说明
table_nameTEXT表名(backup_source / memo / diary…)
order_noINTEGER备份顺序
enabledINTEGER是否启用

方案 B:代码内置表清单——用一个静态数组声明:

staticreadonlyBACKUP_SCOPE:string[]=['backup_source','memo','diary'];

多表遍历备份的核心循环:

for(consttableofBackupDao.BACKUP_SCOPE){constrows=awaitBackupDao.queryTable(context,table);// 按表名查询constjson=JSON.stringify(rows);constfile=`backup_${Date.now()}_${table}.json`;// 每表一个文件awaitBackupDao.writeFile(context,file,json);awaitBackupDao.insertLog(context,{fileName:file,sourceTable:table});}

每张表一个文件 + 一条日志,source_table字段此时真正发挥「区分来源」的作用——日志列表能看出「哪张表、何时、多大」。

十二、备份文件的内容结构:JSON/CSV 的序列化契约

序列化不只是把行拼成字符串,更要在文件里写清楚「这是谁的数据」。JSON 结构带上表名与版本:

{"version":1,"table":"backup_source","exportedAt":1752000000000,"count":12,"rows":[{"id":1,"name":"华为 Mate 60 Pro","category":"数码","amount":6999}]}

version 字段的价值:未来表结构加字段(如新增 price),旧备份恢复时靠 version 决定兼容策略——文件自带版本号,恢复才有升级空间

CSV 的契约则简单:首行表头即结构定义。表头是 CSV 的「元数据」——列名、列序都在第一行,解析时按表头映射字段,恢复就不怕列顺序变化。

十三、恢复流程设计预览

恢复是备份的逆过程,数据层设计上分三步:

  1. 读文件readSync读出 JSON 文本(对称于第七节的 writeSync);
  2. 清空数据源表DELETE FROM backup_source,保证恢复后是「干净的快照」而非新旧混杂;
  3. 事务批量插入BEGIN→ 逐行 insert →COMMIT,任一失败ROLLBACK回滚——恢复的原子性靠事务兜底
// 恢复伪代码:三步走consttext=awaitBackupDao.readFile(context,filePath);// ① 读constparsed=JSON.parse(text);conststore=awaitBackupDao.getStore(context);store.beginTransaction();// ② 清 + ③ 插(事务内)awaitstore.executeSql(`DELETE FROM${BackupDao.SOURCE_TABLE}`);for(constrowofparsed.rows){awaitBackupDao.insertSource(store,row);}store.commit();

事务是恢复的「后悔药」——中途失败整体回滚,数据源表保持原样,不会出现「删了一半、插了一半」的中间态。完整实现见 10-3。

十四、FAQ

Q1:备份日志为什么不存内容?
内容在沙箱文件里,backup_log 只存索引(文件名、大小、时间)。数据库负责「查得快」,文件负责「存得下」——两者职责分离。

Q2:file_name 用时间戳命名会冲突吗?
同一毫秒内连续两次备份理论上会重名,但用户手动操作不可能在 1ms 内点两次导出,实际可忽略;若追求极端安全,可在文件名后追加随机数。

Q3:恢复时为什么必须清空旧数据?
恢复语义是「把备份时点的状态还原回来」,不清空会导致新旧数据叠加(重复条目)。备份是快照,恢复就是整体替换,不是合并。

Q4:JSON 和 CSV 该怎么选?
JSON 给程序恢复用(结构完整、可含类型信息);CSV 给人看用(Excel 直接打开)。生产环境可同时导出两种格式,日志 type 字段区分。

Q5:表结构改了,旧备份还能恢复吗?
能恢复但可能缺列。方案:备份文件带 version 字段,恢复时按版本做字段映射(缺的列填默认值)——这是「文件版本化」设计解决 schema 演进问题的思路。

返回列表