开发文档|“简记事”中鸿蒙关系型数据库(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 权限必须带 reason 与 usedScene,且 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 同时包含id和isPinned,当置顶状态变化时强制重建卡片,图标立即更新。
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用@Prop,ForEachkey 包含isPinned,保存后await viewModel.loadNotes()。 - user_grant 校验失败:
reason必须$string:KEY,usedScene填abilities与when。 - RDB 查询为空:确保初始化顺序与必要的
Promise<void>等待(建表/样例插入后再查)。 - ArkTS 规则错误:遵循本节规范(catch/泛型/强类型字面量/装饰器)。
十二、重要变更与影响
RDBStoreUtil统一封装 RDB 的 DDL/CRUD/查询。NoteViewModel/PrivateNoteViewModel负责初始化与数据拉取,UI 仅绑定。NoteCard改用@Prop,ForEachkey 含isPinned,图标刷新即时生效。- 保存后
await viewModel.loadNotes(),列表排序与图标立即更新。