实例:数据备份导出(Backup)|技术:备份目标库设计、BackupDao 封装
一、业务需求分析:备份导出的本质
数据是应用的「身家性命」——用户记的账、写的日记、存的联系人,丢了就是灾难。备份导出(Backup/Restore)是数据治理的核心能力,业务需求拆解:
- 导出:把数据源表的数据序列化成 JSON / CSV 文本,写入应用沙箱文件;
- 记录备份:每次导出生成一条备份日志(文件名、大小、时间、格式),可追溯;
- 恢复导入:读取备份内容,清空数据源表后重新导入(事务保证原子);
- 删除备份:删除备份记录 + 沙箱文件(双删除);
- 终端化体验:页面用「终端控制台」风格展示操作日志——像黑客帝国一样酷。
技术栈:本实例首次引入文件读写(@kit.CoreFileKit的fileIo)——数据库能力 + 文件系统的结合,让「数据离开数据库变成文件,再从文件回到数据库」的闭环成立。
二、双表结构设计:备份日志表 + 数据源表
备份日志表 backup_log
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| file_name | TEXT | NOT NULL | 备份文件名(backup_时间戳.json) |
| size | INTEGER | NOT NULL DEFAULT 0 | 文件大小(字节) |
| source_table | TEXT | NOT NULL | 数据源表名 |
| backup_time | INTEGER | NOT NULL | 备份时间戳 |
| type | TEXT | NOT NULL DEFAULT ‘json’ | 格式:json / csv |
数据源表 backup_source
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| name | TEXT | NOT NULL | 条目名称 |
| category | TEXT | DEFAULT ‘’ | 分类 |
| amount | REAL | NOT NULL DEFAULT 0 | 金额 |
| note | TEXT | DEFAULT ‘’ | 备注 |
| created_time | INTEGER | NOT 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 | 沙箱落盘 |
| OpenMode | READ_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_name | backup_1752xxx.json | 时间戳命名天然唯一,无需 UUID | 列表展示、删除时定位文件 |
| source_table | backup_source | 记录「这次备份的是哪张表」 | 多表备份时区分日志归属 |
| size | 1248(字节) | 文件大小,列表展示「占多大空间」 | 排序、容量感知 |
| backup_time | 1752xxx(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_name | TEXT | 表名(backup_source / memo / diary…) |
| order_no | INTEGER | 备份顺序 |
| enabled | INTEGER | 是否启用 |
方案 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 的「元数据」——列名、列序都在第一行,解析时按表头映射字段,恢复就不怕列顺序变化。
十三、恢复流程设计预览
恢复是备份的逆过程,数据层设计上分三步:
- 读文件:
readSync读出 JSON 文本(对称于第七节的 writeSync); - 清空数据源表:
DELETE FROM backup_source,保证恢复后是「干净的快照」而非新旧混杂; - 事务批量插入:
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 演进问题的思路。