Skip to content

开发文档|“简记事”中鸿蒙关系型数据库(RDB)的集成与实现

更新: 2025/8/19 字数: 0 字 时长: 0 分钟

本文档详细说明在“简记事”应用中集成与使用关系型数据库(RDB)的完整流程、代码结构与实现细节,覆盖权限配置、数据结构与建表、RDB 读写 API、ViewModel 数据流、UI 绑定与刷新策略、错误与编译规则处理等关键环节。阅读顺序建议与实现顺序一致。


一、目标与范围

  • 使用 @kit.ArkData.relationalStore 在应用内持久化存储笔记。
  • 支持公开/隐私笔记、置顶与排序、搜索筛选、CRUD 全流程。
  • 确保 UI 与数据库异步操作正确联动,状态与排序即时刷新。

二、环境与前置条件

  • Stage 模型,API ≥ 6.0.0(20)
  • 关键 Kit:
    • 数据库:@kit.ArkData(relationalStore)
    • 日志:@kit.PerformanceAnalysisKit(hilog)
    • 错误类型:@kit.BasicServicesKit(BusinessError)
    • 偏好:@ohos.data.preferences(首启协议标识)
    • UI/路由:@kit.ArkUI
  • ArkTS 规范与误用约束(文末“错误处理与规范要点”章节中详述)

三、权限与资源配置

user_grant 权限必须带 reasonusedScene,且 reason 必须 $string:KEY 资源引用。

  • entry/src/main/module.json5
json
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet", "2in1"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" },
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:perm_reason_read_media",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "$string:perm_reason_write_media",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      }
    ]
  }
}
  • entry/src/main/resources/base/element/string.json
json
{
  "string": [
    { "name": "module_desc", "value": "module description" },
    { "name": "EntryAbility_desc", "value": "description" },
    { "name": "EntryAbility_label", "value": "label" },
    { "name": "perm_reason_read_media", "value": "用于读取应用内笔记相关的媒体与数据库文件,保障数据呈现与备份恢复。" },
    { "name": "perm_reason_write_media", "value": "用于写入应用内笔记相关的媒体与数据库文件,保障记事创建与编辑功能。" }
  ]
}

四、数据模型与建表

4.1 笔记数据模型

  • entry/src/main/ets/model/TxtItem.ets
ts
export class TxtItem {
  id: number;
  title: string;
  content: string;
  createTime: number;
  color: string;
  isPinned: boolean;
  isPrivate: boolean;

  constructor(id: number, title: string, content: string, color: string, isPinned: boolean = false, isPrivate: boolean = false, createTime?: number) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.createTime = createTime ?? new Date().getTime();
    this.color = color;
    this.isPinned = isPinned;
    this.isPrivate = isPrivate;
  }
}

4.2 SQL 常量

  • 排序:置顶优先(IS_PINNED DESC)、时间倒序(CREATE_TIME DESC)。
  • entry/src/main/ets/common/constants/CommonConstants.ets
ts
static readonly CREATE_NOTES_TABLE_SQL: string =
  'CREATE TABLE IF NOT EXISTS NOTES (ID INTEGER PRIMARY KEY AUTOINCREMENT, TITLE TEXT ' +
    'NOT NULL, CONTENT TEXT NOT NULL, CREATE_TIME INTEGER NOT NULL, COLOR TEXT NOT NULL, ' +
    'IS_PINNED INTEGER NOT NULL DEFAULT 0, IS_PRIVATE INTEGER NOT NULL DEFAULT 0)';

static readonly QUERY_ALL_PUBLIC_NOTES_SQL: string =
  'SELECT * FROM NOTES WHERE IS_PRIVATE = 0 ORDER BY IS_PINNED DESC, CREATE_TIME DESC';

static readonly QUERY_ALL_PRIVATE_NOTES_SQL: string =
  'SELECT * FROM NOTES WHERE IS_PRIVATE = 1 ORDER BY IS_PINNED DESC, CREATE_TIME DESC';

static readonly QUERY_NOTES_BY_SEARCH_SQL: string =
  'SELECT * FROM NOTES WHERE (TITLE LIKE ? OR CONTENT LIKE ?) AND IS_PRIVATE = ? ORDER BY IS_PINNED DESC, CREATE_TIME DESC';

static readonly QUERY_NOTE_BY_ID_SQL: string = 'SELECT * FROM NOTES WHERE ID = ?';
static readonly INSERT_NOTE_SQL: string =
  'INSERT INTO NOTES (TITLE, CONTENT, CREATE_TIME, COLOR, IS_PINNED, IS_PRIVATE) VALUES (?, ?, ?, ?, ?, ?)';
static readonly UPDATE_NOTE_SQL: string =
  'UPDATE NOTES SET TITLE = ?, CONTENT = ?, COLOR = ?, IS_PINNED = ? WHERE ID = ?';
static readonly DELETE_NOTE_SQL: string = 'DELETE FROM NOTES WHERE ID = ?';

五、RDB 工具层(RDBStoreUtil)

职责:集中管理 RDB 连接、建表、样例数据初始化,以及 CRUD/查询,和 UI/VM 解耦。

  • entry/src/main/ets/common/database/RDBStoreUtil.ets

5.1 获取 RdbStore

ts
createNotesRDB(context: Context) {
  const STORE_CONFIG: relationalStore.StoreConfig = { name: 'Notes.db', securityLevel: relationalStore.SecurityLevel.S1 };
  relationalStore.getRdbStore(context, STORE_CONFIG, (err: BusinessError, rdbStore: relationalStore.RdbStore) => {
    this.notesRDB = rdbStore;
    if (err) {
      hilog.error(0x0000, TAG, `Get RdbStore failed, code is ${err.code},message is ${err.message}`);
      return;
    }
    hilog.info(0x0000, TAG, 'Get RdbStore successfully.');
  })
}

5.2 表存在性检查与建表

ts
async checkNotesTableExists(): Promise<boolean> {
  try {
    const result = await this.notesRDB?.querySql("SELECT name FROM sqlite_master WHERE type='table' AND name='NOTES'");
    if (result) { const exists = result.goToNextRow(); result.close(); return exists; }
    return false;
  } catch (e) { return false; }
}

createNotesTable() {
  this.notesRDB?.execute(CommonConstants.CREATE_NOTES_TABLE_SQL)
    .then(() => hilog.info(0x0000, TAG, `execute create notes table sql success`))
    .catch((err: BusinessError) => hilog.error(0x0000, TAG, `execute sql failed, code is ${err.code},message is ${err.message}`));
}

5.3 初始化样例数据(强类型 ValuesBucket + 批量插入)

ts
initSampleNotes() {
  const noteOne: relationalStore.ValuesBucket = { 'TITLE': '欢迎使用卡片记事本', 'CONTENT': '...', 'CREATE_TIME': Date.now(), 'COLOR': '#FFC4C4', 'IS_PINNED': 1, 'IS_PRIVATE': 0 };
  const noteTwo: relationalStore.ValuesBucket = { 'TITLE': '购物清单', 'CONTENT': '...', 'CREATE_TIME': Date.now() - 100000, 'COLOR': '#FFF5C3', 'IS_PINNED': 0, 'IS_PRIVATE': 0 };
  const noteThree: relationalStore.ValuesBucket = { 'TITLE': '学习目标', 'CONTENT': '...', 'CREATE_TIME': Date.now() - 200000, 'COLOR': '#D9D2E9', 'IS_PINNED': 0, 'IS_PRIVATE': 0 };
  const noteFour: relationalStore.ValuesBucket = { 'TITLE': '我的秘密日记', 'CONTENT': '...', 'CREATE_TIME': Date.now() - 300000, 'COLOR': '#FFD180', 'IS_PINNED': 0, 'IS_PRIVATE': 1 };

  const valueBuckets = new Array(noteOne, noteTwo, noteThree, noteFour);
  this.notesRDB?.batchInsert('NOTES', valueBuckets)
    .then((insertNum: number) => hilog.info(0x0000, TAG, `batchInsert is successful, the number of values that were inserted = ${insertNum}`))
    .catch((err: BusinessError) => {
      if (err.code === 14800001) { hilog.info(0x0000, TAG, 'Sample data already exists, skipping insertion'); }
      else { hilog.error(0x0000, TAG, `batchInsert is failed, code is ${err.code},message is ${err.message}`); }
    })
}

5.4 查询与 CRUD(注意 ResultSet 需 close)

ts
async queryAllPublicNotes(): Promise<TxtItem[]> {
  const notesSet: Array<TxtItem> = [];
  await this.notesRDB?.querySql(CommonConstants.QUERY_ALL_PUBLIC_NOTES_SQL)
    .then((resultSet: relationalStore.ResultSet) => {
      while (resultSet.goToNextRow()) {
        const id = resultSet.getValue(resultSet.getColumnIndex('ID')) as number;
        const title = resultSet.getValue(resultSet.getColumnIndex('TITLE')) as string;
        const content = resultSet.getValue(resultSet.getColumnIndex('CONTENT')) as string;
        const createTime = resultSet.getValue(resultSet.getColumnIndex('CREATE_TIME')) as number;
        const color = resultSet.getValue(resultSet.getColumnIndex('COLOR')) as string;
        const isPinned = resultSet.getValue(resultSet.getColumnIndex('IS_PINNED')) as number;
        const isPrivate = resultSet.getValue(resultSet.getColumnIndex('IS_PRIVATE')) as number;
        notesSet.push(new TxtItem(id, title, content, color, isPinned === 1, isPrivate === 1, createTime));
      }
      resultSet.close();
    })
    .catch((err: BusinessError) => hilog.error(0x0000, TAG, `Query failed, code is ${err.code},message is ${err.message}`));
  return notesSet;
}

async insertNote(note: TxtItem): Promise<number> {
  const noteData: relationalStore.ValuesBucket = {
    'TITLE': note.title,
    'CONTENT': note.content,
    'CREATE_TIME': note.createTime,
    'COLOR': note.color,
    'IS_PINNED': note.isPinned ? 1 : 0,
    'IS_PRIVATE': note.isPrivate ? 1 : 0
  };
  return await this.notesRDB?.insert('NOTES', noteData, relationalStore.ConflictResolution.ON_CONFLICT_REPLACE)
    .then((rowId: number) => { hilog.info(0x0000, TAG, `Insert is successful, rowId = ${rowId}`); return rowId; })
    .catch((err: BusinessError) => { hilog.error(0x0000, TAG, `Insert is failed, code is ${err.code},message is ${err.message}`); return -1; }) ?? -1;
}

六、ViewModel 层设计

  • 负责:初始化数据库、拉取/筛选数据、CRUD 并同步内存数组。
  • 使用 @Observed class,内部存 notes: TxtItem[];UI 绑定 VM 数据或同步到 @State

6.1 公共笔记 VM(私有 VM 同样逻辑)

  • entry/src/main/ets/viewmodel/NoteViewModel.ets
ts
@Observed
class NoteViewModel {
  notes: TxtItem[] = [];
  private isInitialized: boolean = false;

  async initializeDatabase(context: Context) {
    if (this.isInitialized) return;
    if (!rdbStoreUtil.isInitialized()) { rdbStoreUtil.createNotesRDB(context); }

    const tableExists = await rdbStoreUtil.checkNotesTableExists();
    if (!tableExists) {
      rdbStoreUtil.createNotesTable();
      await new Promise<void>((resolve) => setTimeout(resolve, 100));
    }
    const existingNotes = await rdbStoreUtil.queryAllPublicNotes();
    if (existingNotes.length === 0) {
      rdbStoreUtil.initSampleNotes();
      await new Promise<void>((resolve) => setTimeout(resolve, 100));
    }
    this.isInitialized = true;
    await this.loadNotes();
  }

  async loadNotes() { this.notes = await rdbStoreUtil.queryAllPublicNotes(); }
  async searchNotes(searchText: string) {
    if (searchText.trim() === '') { this.notes = await rdbStoreUtil.queryAllPublicNotes(); }
    else { this.notes = await rdbStoreUtil.queryNotesBySearch(searchText, false); }
  }

  async addNote(note: TxtItem) {
    const newNote = new TxtItem(0, note.title, note.content, note.color, note.isPinned, false);
    const rowId = await rdbStoreUtil.insertNote(newNote);
    if (rowId > 0) { newNote.id = rowId; this.notes.unshift(newNote); }
  }

  async deleteNote(noteId: number) {
    const success = await rdbStoreUtil.deleteNote(noteId);
    if (success) { this.notes = this.notes.filter(n => n.id !== noteId); }
  }

  async updateNote(updatedNote: TxtItem) {
    const success = await rdbStoreUtil.updateNote(updatedNote);
    if (success) {
      const i = this.notes.findIndex(n => n.id === updatedNote.id);
      if (i !== -1) { this.notes[i] = updatedNote; }
    }
  }
}

6.2 私有笔记 VM(差异在 isPrivate 与查询函数)

  • entry/src/main/ets/viewmodel/PrivateNoteViewModel.ets
ts
@Observed
class PrivateNoteViewModel {
  notes: TxtItem[] = [];
  private isInitialized: boolean = false;

  async initializeDatabase(context: Context) {
    if (this.isInitialized) return;
    if (!rdbStoreUtil.isInitialized()) { rdbStoreUtil.createNotesRDB(context); }

    const tableExists = await rdbStoreUtil.checkNotesTableExists();
    if (!tableExists) {
      rdbStoreUtil.createNotesTable();
      await new Promise<void>((resolve) => setTimeout(resolve, 100));
    }
    const existingNotes = await rdbStoreUtil.queryAllPrivateNotes();
    if (existingNotes.length === 0) {
      await new Promise<void>((resolve) => setTimeout(resolve, 100));
    }
    this.isInitialized = true;
    await this.loadNotes();
  }

  async loadNotes() { this.notes = await rdbStoreUtil.queryAllPrivateNotes(); }
  async searchNotes(searchText: string) {
    if (searchText.trim() === '') { this.notes = await rdbStoreUtil.queryAllPrivateNotes(); }
    else { this.notes = await rdbStoreUtil.queryNotesBySearch(searchText, true); }
  }
}

七、UI 集成与刷新策略

7.1 首页初始化与首启偏好检查

  • onPageShow 中初始化 RDB、绑定本地状态、执行筛选与刷新;首启通过 @ohos.data.preferences 检查并弹窗。
  • 公开/私有切换:重置搜索词,调用对应过滤;公开页自动刷新诗词。

7.2 列表渲染——置顶图标即时更新

  • 关键点:

    • NoteCard 使用 @Prop 接受 note,父层传入的新值即时反映;
    • ForEach 的 key 同时包含 idisPinned,当置顶状态变化时强制重建卡片,图标立即更新。
  • entry/src/main/ets/view/NoteCard.ets

ts
@Component
export struct NoteCard {
  @Prop note: TxtItem;

  build() {
    Column() {
      Stack({ alignContent: Alignment.TopEnd }) {
        Text(this.note.title)
        if (this.note.isPinned) {
          Stack() {
            Circle().width(20).height(20).fill(Color.White).opacity(0.8)
            Image($r("app.media.ic_pin")).width(16).height(16).fillColor(APP_THEME_COLOR)
          }
          .margin({ top: 4, right: 4 })
          .animation({ duration: 300, curve: Curve.EaseInOut })
        }
      }
      // ... 省略内容文本、时间与容器样式
    }
  }
}
  • entry/src/main/ets/pages/Index.ets(公开列表示例,私有同理)
ts
ForEach(this.publicNotes, (item: TxtItem) => {
  FlowItem() {
    NoteCard({ note: item })
      .onClick(() => router.pushUrl({ url: 'pages/NoteEditPage', params: { note: item, isPrivate: false } }))
  }.gesture(LongPressGesture().onAction(() => this.showDeleteDialog(item, false)))
}, (item: TxtItem) => `${item.id}-${item.isPinned ? 1 : 0}`)

7.3 编辑页保存后强制刷新排序

  • 更新/新增成功后立即 await viewModel.loadNotes(),再 router.back(),首页按 SQL 规则即时重排,置顶图标也随 key 刷新。
  • entry/src/main/ets/pages/NoteEditPage.ets
ts
(async () => {
  if (this.note) {
    this.note.title = this.title;
    this.note.content = this.content;
    this.note.color = this.selectedColor;
    this.note.isPinned = this.isPinned;
    await viewModel.updateNote(this.note);
    await viewModel.loadNotes();
  } else {
    const newNote = new TxtItem(0, this.title, this.content, this.selectedColor, this.isPinned, this.isPrivate);
    await viewModel.addNote(newNote);
    await viewModel.loadNotes();
  }
  router.back();
})();

八、首启协议与偏好

  • 使用 @ohos.data.preferences 读写 policy_agreed
  • 首次启动未同意:打开自定义弹窗;同意写入并 flush();拒绝则 UIAbilityContext.terminateSelf() 退出。
  • 首页 onPageShow 中进行检查与控制。

九、错误处理与 ArkTS 规范要点

  • catch 不写类型注解;内部用 (e as Error)?.message ?? String(e)
  • new Promise(...) 显式 <void>new Promise<void>((resolve) => setTimeout(resolve, 100))
  • batchInsert 传入 ValuesBucket[] 强类型数组,避免“未类型化字面量”报错。
  • @State 仅用于 struct;VM 用 @Observed class
  • ResultSet 遍历后必须 close(),避免资源泄漏。
  • user_grant 权限 reason 必须 $string:KEY 且补充 usedScene

十、手工验证清单

  • 首次进入:自动建库/建表/初始化样例,首页显示笔记。
  • 新增笔记:保存后即时出现在列表顶部(排序符合置顶/时间规则)。
  • 置顶/取消置顶:编辑页切换后保存返回,卡片位置与右上角图标即时变化。
  • 搜索:公开/隐私页关键字过滤,排序稳定。
  • 删除:长按 -> 确认 -> 立即从列表移除。
  • 隐私页:与公开页一致的 CRUD 与刷新体验。
  • 重启应用:数据持久,排序正确。

十一、常见问题与处理

  • 置顶图标不刷新:确保 NoteCard@PropForEach key 包含 isPinned,保存后 await viewModel.loadNotes()
  • user_grant 校验失败:reason 必须 $string:KEYusedSceneabilitieswhen
  • RDB 查询为空:确保初始化顺序与必要的 Promise<void> 等待(建表/样例插入后再查)。
  • ArkTS 规则错误:遵循本节规范(catch/泛型/强类型字面量/装饰器)。

十二、重要变更与影响

  • RDBStoreUtil 统一封装 RDB 的 DDL/CRUD/查询。
  • NoteViewModel/PrivateNoteViewModel 负责初始化与数据拉取,UI 仅绑定。
  • NoteCard 改用 @PropForEach key 含 isPinned,图标刷新即时生效。
  • 保存后 await viewModel.loadNotes(),列表排序与图标立即更新。
本站访客数 人次 本站总访问量