基于RDB实现笔记本地持久化,做好端云同步的数据底座
【摘要】 前言在笔记类应用中,不管后续要不要对接云端同步,本地数据可靠存储都是绕不开的底层能力。很多开发者上手时会直接选择Preferences,但Preferences更适合存放简单键值配置,不适合结构化、存在关联关系、未来需要同步的数据。本文聚焦一个核心实现:使用HarmonyOS关系型数据库(RDB)搭建笔记的数据层,设计适配后续端云同步的数据模型,完成基础的增删改查。本文只聊技术实现、表结构...
前言
在笔记类应用中,不管后续要不要对接云端同步,本地数据可靠存储都是绕不开的底层能力。很多开发者上手时会直接选择Preferences,但Preferences更适合存放简单键值配置,不适合结构化、存在关联关系、未来需要同步的数据。
本文聚焦一个核心实现:使用HarmonyOS关系型数据库(RDB)搭建笔记的数据层,设计适配后续端云同步的数据模型,完成基础的增删改查。本文只聊技术实现、表结构设计、编码要点,以及为什么这样设计更利于后续扩展同步能力。
选型说明:为什么选择RDB而不是Preferences
- Preferences:适合简单KV,不支持复杂查询、排序、分页,没有事务,不适合大量笔记数据;
- RDB(RelationalStore):底层基于SQLite,支持事务、索引、条件分页查询,支持结构化字段设计;
- 关键优势:可以在设计表结构时预留同步字段,不需要后期大幅度改表迁移数据。
适用场景:笔记正文、标签、修改时间、版本标记这类结构化数据;图片、录音等二进制文件不入库,只在库中保存文件路径。
数据模型设计(适配端云同步)
这是最关键的部分。如果只做本地笔记,自增ID够用;但要为后续同步预留能力,就不能用自增主键。
CREATE TABLE note (
id TEXT PRIMARY KEY, -- 全局唯一ID,UUID,跨设备唯一,同步核心
title TEXT,
content TEXT,
create_time INTEGER, -- 毫秒时间戳
update_time INTEGER,
is_deleted INTEGER DEFAULT 0, -- 软删除标记,同步时处理删除
version INTEGER DEFAULT 1, -- 数据版本号,用于冲突判定
extra TEXT -- JSON字符串,扩展字段,标签、附件路径等
);
CREATE INDEX idx_note_update_time ON note(update_time);
字段设计要点:
- 主键id使用UUID,避免多端自增ID冲突;
is_deleted软删除:同步场景下不能直接DELETE,否则其他端无法感知删除动作;version版本号:每次修改+1,拉取云端数据时对比版本,作为冲突判断依据;- 二进制资源(图片、音频)存应用沙箱,数据库只存uri或者沙箱相对路径;
- 建立更新时间索引,方便增量同步:只拉取大于本地最大update_time的数据。
工程结构分层
src/main/ets
├── model
│ └── Note.ets // 笔记实体类
├── service
│ └── RdbNoteService.ets// RDB数据库封装,CRUD、初始化
├── utils
│ └── uuid.ets // UUID工具
代码实现
1. 笔记实体 Note.ets
export interface Note {
id: string;
title: string;
content: string;
createTime: number;
updateTime: number;
isDeleted: number;
version: number;
extra: string; // 存放JSON,例如 {tags: ["学习"], attachments: ["xxx.uri"]}
}
2. RDB封装服务 RdbNoteService.ets
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
const STORE_CONFIG: relationalStore.StoreConfig = {
name: "NoteDatabase.db",
securityLevel: relationalStore.SecurityLevel.S1
};
export class RdbNoteService {
private rdbStore: relationalStore.RdbStore | null = null;
async init(): Promise<void> {
this.rdbStore = await relationalStore.getRdbStore(STORE_CONFIG);
const sql = `CREATE TABLE IF NOT EXISTS note (
id TEXT PRIMARY KEY,
title TEXT,
content TEXT,
create_time INTEGER,
update_time INTEGER,
is_deleted INTEGER DEFAULT 0,
version INTEGER DEFAULT 1,
extra TEXT
);
CREATE INDEX IF NOT EXISTS idx_note_update_time ON note(update_time);`;
await this.rdbStore.executeSql(sql);
}
// 新增笔记
async addNote(note: Note): Promise<number> {
if (!this.rdbStore) throw new Error("rdb not init");
const valueBucket: relationalStore.ValuesBucket = {
"id": note.id,
"title": note.title,
"content": note.content,
"create_time": note.createTime,
"update_time": note.updateTime,
"is_deleted": note.isDeleted,
"version": note.version,
"extra": note.extra
};
return this.rdbStore.insert("note", valueBucket);
}
// 更新笔记,版本号+1
async updateNote(note: Note): Promise<number> {
if (!this.rdbStore) throw new Error("rdb not init");
const valueBucket: relationalStore.ValuesBucket = {
"title": note.title,
"content": note.content,
"update_time": new Date().getTime(),
"version": note.version + 1,
"extra": note.extra
};
const predicates = new relationalStore.RdbPredicates("note");
predicates.equalTo("id", note.id);
return this.rdbStore.update(valueBucket, predicates);
}
// 查询未删除笔记,按更新时间倒序
async queryAllValid(): Promise<Note[]> {
if (!this.rdbStore) throw new Error("rdb not init");
const predicates = new relationalStore.RdbPredicates("note");
predicates.equalTo("is_deleted", 0).orderByDesc("update_time");
const resultSet = await this.rdbStore.query(predicates);
const list: Note[] = [];
while (resultSet.goToNextRow()) {
list.push({
id: resultSet.getString(resultSet.getColumnIndex("id")),
title: resultSet.getString(resultSet.getColumnIndex("title")),
content: resultSet.getString(resultSet.getColumnIndex("content")),
createTime: resultSet.getLong(resultSet.getColumnIndex("create_time")),
updateTime: resultSet.getLong(resultSet.getColumnIndex("update_time")),
isDeleted: resultSet.getLong(resultSet.getColumnIndex("is_deleted")),
version: resultSet.getLong(resultSet.getColumnIndex("version")),
extra: resultSet.getString(resultSet.getColumnIndex("extra"))
})
}
resultSet.close();
return list;
}
// 软删除
async softDeleteNote(id: string): Promise<number> {
if (!this.rdbStore) throw new Error("rdb not init");
const bucket: relationalStore.ValuesBucket = {
"is_deleted": 1,
"update_time": new Date().getTime(),
"version": (await this.getNoteById(id))?.version + 1
};
const pred = new relationalStore.RdbPredicates("note");
pred.equalTo("id", id);
return this.rdbStore.update(bucket, pred);
}
async getNoteById(id: string): Promise<Note | null> {
if (!this.rdbStore) throw new Error("rdb not init");
const pred = new relationalStore.RdbPredicates("note");
pred.equalTo("id", id);
const rs = await this.rdbStore.query(pred);
if (rs.goToFirstRow()) {
const note: Note = {
id: rs.getString(rs.getColumnIndex("id")),
title: rs.getString(rs.getColumnIndex("title")),
content: rs.getString(rs.getColumnIndex("content")),
createTime: rs.getLong(rs.getColumnIndex("create_time")),
updateTime: rs.getLong(rs.getColumnIndex("update_time")),
isDeleted: rs.getLong(rs.getColumnIndex("is_deleted")),
version: rs.getLong(rs.getColumnIndex("version")),
extra: rs.getString(rs.getColumnIndex("extra"))
}
rs.close();
return note;
}
rs.close();
return null;
}
}
3. 页面调用示例
import { RdbNoteService } from '../service/RdbNoteService';
import { v4 as uuidv4 } from 'uuid';
@Entry
@Component
struct NoteListPage {
noteService: RdbNoteService = new RdbNoteService();
@State noteList: Note[] = [];
async aboutToAppear() {
await this.noteService.init();
this.noteList = await this.noteService.queryAllValid();
}
async addNewNote() {
const now = new Date().getTime();
const newNote: Note = {
id: uuidv4(),
title: "新建笔记",
content: "",
createTime: now,
updateTime: now,
isDeleted: 0,
version: 1,
extra: JSON.stringify({ tags: [], attachments: [] })
}
await this.noteService.addNote(newNote);
this.noteList = await this.noteService.queryAllValid();
}
build() {
Column() {
List() {
ForEach(this.noteList, item => {
ListItem() {
Text(item.title)
}
})
}
Button("新增笔记").onClick(() => this.addNewNote())
}.width('100%').height('100%')
}
}
关键技术要点与坑点
- 安全等级S1:本示例用S1,适合应用私有数据;如果需要跨设备、其他场景,要根据官方安全等级规范调整;
- ResultSet必须close,否则会出现游标泄漏,长时间使用数据库卡顿;
- 版本号只在本地修改动作时递增,同步逻辑里比较版本决定覆盖策略;
- 附件路径:沙箱路径不能直接硬编码保存,优先使用relative路径,在读取时拼接应用沙箱根目录;
- 事务:批量同步拉取写入时,建议包裹事务,保证一批数据要么全部写入,要么回滚,避免本地数据状态错乱。
后续扩展方向(技术路线)
这套RDB底座稳定之后,上层能力可以按顺序叠加:
- ArkUI编辑器:富文本、图片、录音附件选择;
- 增量同步:拿本地最大updateTime作为游标,向云端拉取更新;
- 冲突策略:版本号对比,支持【服务端优先 / 本地优先 / 弹窗让用户选择】;
- 标签表独立拆表,做多对多关联;
- 本地通知提醒能力对接。
小结
笔记应用的数据层,核心效果目标是:本地数据可靠持久化,同时数据模型原生支持未来端云同步,不需要后期重构表结构。
通过RDB + UUID主键 + 版本号 + 软删除这套方案,我们得到的不只是一个能存笔记的本地数据库,而是一个具备同步扩展能力的数据底座。
UI编辑器、录音、图片这些功能是表层体验,稳定可扩展的数据模型,才是这类应用的基石。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)