基于RDB实现笔记本地持久化,做好端云同步的数据底座

举报
yd_284811271 发表于 2026/11/08 18:35:05 2026/11/08
【摘要】 前言在笔记类应用中,不管后续要不要对接云端同步,本地数据可靠存储都是绕不开的底层能力。很多开发者上手时会直接选择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);

字段设计要点:

  1. 主键id使用UUID,避免多端自增ID冲突;
  2. is_deleted 软删除:同步场景下不能直接DELETE,否则其他端无法感知删除动作;
  3. version 版本号:每次修改+1,拉取云端数据时对比版本,作为冲突判断依据;
  4. 二进制资源(图片、音频)存应用沙箱,数据库只存uri或者沙箱相对路径;
  5. 建立更新时间索引,方便增量同步:只拉取大于本地最大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%')
  }
}

关键技术要点与坑点

  1. 安全等级S1:本示例用S1,适合应用私有数据;如果需要跨设备、其他场景,要根据官方安全等级规范调整;
  2. ResultSet必须close,否则会出现游标泄漏,长时间使用数据库卡顿;
  3. 版本号只在本地修改动作时递增,同步逻辑里比较版本决定覆盖策略;
  4. 附件路径:沙箱路径不能直接硬编码保存,优先使用relative路径,在读取时拼接应用沙箱根目录;
  5. 事务:批量同步拉取写入时,建议包裹事务,保证一批数据要么全部写入,要么回滚,避免本地数据状态错乱。

后续扩展方向(技术路线)

这套RDB底座稳定之后,上层能力可以按顺序叠加:

  1. ArkUI编辑器:富文本、图片、录音附件选择;
  2. 增量同步:拿本地最大updateTime作为游标,向云端拉取更新;
  3. 冲突策略:版本号对比,支持【服务端优先 / 本地优先 / 弹窗让用户选择】;
  4. 标签表独立拆表,做多对多关联;
  5. 本地通知提醒能力对接。

小结

笔记应用的数据层,核心效果目标是:本地数据可靠持久化,同时数据模型原生支持未来端云同步,不需要后期重构表结构。
通过RDB + UUID主键 + 版本号 + 软删除这套方案,我们得到的不只是一个能存笔记的本地数据库,而是一个具备同步扩展能力的数据底座。
UI编辑器、录音、图片这些功能是表层体验,稳定可扩展的数据模型,才是这类应用的基石。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。