ArkData 方舟数据管理:从存数据到安全共享的完整实战指南

适用版本:HarmonyOS 5.0.0 / HarmonyOS NEXT
难度:入门到进阶
预计阅读时间:25 分钟
写在前面
几乎每个应用都需要"记住一些东西"。
用户选了暗色模式,你记住了吗?用户填了一半的表单,他切到微信回个消息再回来,数据还在吗?用户换了新手机,你应用里的数据能跟着过去吗?
这些看似基础的问题,恰恰是衡量一个应用"能不能用"的核心标准。鸿蒙为此提供了一套完整的数据管理方案——ArkData。它不是某一个 API,而是从"怎么存"到"怎么安全共享"的一整套能力体系。
这篇文章我会用一个完整的笔记应用作为贯穿例子,把 ArkData 的四个核心模块——持久化、标准化数据、跨应用共享、安全可靠性——全部串起来讲清楚。你读完之后,应该知道你的应用在什么场景下该用什么方案,以及怎么写代码。
一、应用数据持久化:你的应用"失忆"了吗
1.1 为什么需要持久化
想象你在做一个记账应用。用户记了一笔"今天买咖啡花了 28 块",这个信息如果只存在内存里,用户切到后台、接个电话、或者系统内存紧张杀了你的应用,这笔记录就消失了。
持久化就是把内存里的数据写到磁盘上,让应用重启后还能找回来。
ArkData 提供了两种最常用的持久化方式:
| 方式 | 适合场景 | 数据类型 | 特点 |
|---|---|---|---|
| Preferences | 配置项、用户偏好、开关状态 | 键值对(key-value) | 简单、轻量、读写快 |
| RDB(关系型数据库) | 结构化数据、多表关联、复杂查询 | 表格(行和列) | 功能强大、支持 SQL、适合大量数据 |
1.2 Preferences:简单配置的首选
Preferences 就是鸿蒙版的"小账本",你不需要写 SQL,不需要定义表结构,就是简单的 key = value。
适用场景: 记住用户是否开启了暗色模式、用户最后一次登录的账号、应用的首次启动标记等。
import { preferences } from '@kit.ArkData';
class AppConfig {
private pref: preferences.Preferences | null = null;
private static instance: AppConfig;
private context: Context;
private constructor(context: Context) {
this.context = context;
}
static async getInstance(context: Context): Promise<AppConfig> {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig(context);
// 创建或打开一个名为 "app_settings" 的 Preferences 文件
AppConfig.instance.pref = await preferences.getPreferences(context, 'app_settings');
}
return AppConfig.instance;
}
// 保存布尔值配置
async setBoolean(key: string, value: boolean): Promise<void> {
await this.pref!.put(key, value);
// 写入后必须调用 flush,否则数据只缓存在内存中
await this.pref!.flush();
}
// 读取布尔值配置,如果不存在返回默认值
async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
return await this.pref!.get(key, defaultValue) as boolean;
}
// 保存字符串配置
async setString(key: string, value: string): Promise<void> {
await this.pref!.put(key, value);
await this.pref!.flush();
}
async getString(key: string, defaultValue: string = ''): Promise<string> {
return await this.pref!.get(key, defaultValue) as string;
}
// 保存数字配置
async setNumber(key: string, value: number): Promise<void> {
await this.pref!.put(key, value);
await this.pref!.flush();
}
async getNumber(key: string, defaultValue: number = 0): Promise<number> {
return await this.pref!.get(key, defaultValue) as number;
}
}
// 使用示例
async function setupUserPreferences(context: Context): Promise<void> {
const config = await AppConfig.getInstance(context);
// 保存用户偏好
await config.setBoolean('dark_mode', true);
await config.setString('last_user', 'zhangsan');
await config.setNumber('font_size', 16);
// 读取用户偏好
const isDarkMode = await config.getBoolean('dark_mode', false);
const lastUser = await config.getString('last_user', '');
const fontSize = await config.getNumber('font_size', 14);
console.info(`当前模式:${isDarkMode ? '暗色' : '亮色'},用户:${lastUser},字体:${fontSize}`);
}
几个重要细节:
-
单例模式: Preferences 应该作为单例使用,不要每次读写都创建新实例,否则会有性能问题和数据不一致风险。
-
flush 必须调用:
put()只是把数据放到内存缓存里,必须调用flush()才会真正写入磁盘。很多新人忘记这一步,以为数据保存了,结果应用重启后数据丢了。 -
不要存大量数据: Preferences 适合几十条配置项,如果你要存几百条笔记、几千条聊天记录,它会变得很慢。那时候就该用 RDB 了。
1.3 RDB 关系型数据库:结构化数据的归宿
当你的数据有复杂的结构、需要查询、排序、过滤、关联时,Preferences 就不够用了。这时候你需要 RDB。
我用一个笔记应用的数据库设计来演示:
import { relationalStore } from '@kit.ArkData';
// 定义数据库配置
const DB_CONFIG: relationalStore.StoreConfig = {
name: 'NotesDB.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1, // 安全级别
encrypt: false // 是否加密(后面会讲安全性时再深入)
};
// 表结构定义
const CREATE_NOTE_TABLE = `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
category TEXT DEFAULT 'uncategorized',
create_time INTEGER,
update_time INTEGER,
is_favorite INTEGER DEFAULT 0,
is_deleted INTEGER DEFAULT 0
)
`;
const CREATE_CATEGORY_TABLE = `
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#999999',
sort_order INTEGER DEFAULT 0
)
`;
class NoteDatabase {
private static instance: NoteDatabase;
private store: relationalStore.RdbStore | null = null;
private context: Context;
private constructor(context: Context) {
this.context = context;
}
static async getInstance(context: Context): Promise<NoteDatabase> {
if (!NoteDatabase.instance) {
NoteDatabase.instance = new NoteDatabase(context);
await NoteDatabase.instance.init();
}
return NoteDatabase.instance;
}
private async init(): Promise<void> {
this.store = await relationalStore.getRdbStore(this.context, DB_CONFIG);
// 创建表
await this.store.executeSql(CREATE_NOTE_TABLE);
await this.store.executeSql(CREATE_CATEGORY_TABLE);
// 插入默认分类
await this.insertDefaultCategories();
}
private async insertDefaultCategories(): Promise<void> {
const defaultCategories = [
{ name: '工作', color: '#2196F3' },
{ name: '生活', color: '#4CAF50' },
{ name: '灵感', color: '#FF9800' },
{ name: '学习', color: '#9C27B0' }
];
for (const cat of defaultCategories) {
const exists = await this.store!.querySql(
`SELECT 1 FROM categories WHERE name = ?`,
[cat.name]
);
if (exists.goToFirstRow() !== true) {
await this.store!.executeSql(
`INSERT INTO categories (name, color) VALUES (?, ?)`,
[cat.name, cat.color]
);
}
exists.close();
}
}
// 插入一条笔记
async insertNote(note: Note): Promise<number> {
const now = Date.now();
const valueBucket: relationalStore.ValuesBucket = {
title: note.title,
content: note.content,
category: note.category,
create_time: now,
update_time: now,
is_favorite: note.isFavorite ? 1 : 0,
is_deleted: 0
};
const result = await this.store!.insert('notes', valueBucket);
return result; // 返回新插入行的 id
}
// 查询所有未删除的笔记,按更新时间倒序
async queryAllNotes(): Promise<Note[]> {
const resultSet = await this.store!.querySql(
`SELECT * FROM notes
WHERE is_deleted = 0
ORDER BY update_time DESC`
);
const notes: Note[] = [];
while (resultSet.goToNextRow() === true) {
notes.push(this.parseNoteFromResultSet(resultSet));
}
resultSet.close();
return notes;
}
// 按分类查询笔记
async queryNotesByCategory(category: string): Promise<Note[]> {
const resultSet = await this.store!.querySql(
`SELECT * FROM notes
WHERE is_deleted = 0 AND category = ?
ORDER BY update_time DESC`,
[category]
);
const notes: Note[] = [];
while (resultSet.goToNextRow() === true) {
notes.push(this.parseNoteFromResultSet(resultSet));
}
resultSet.close();
return notes;
}
// 搜索笔记(标题或内容包含关键词)
async searchNotes(keyword: string): Promise<Note[]> {
const pattern = `%${keyword}%`;
const resultSet = await this.store!.querySql(
`SELECT * FROM notes
WHERE is_deleted = 0 AND (title LIKE ? OR content LIKE ?)
ORDER BY update_time DESC`,
[pattern, pattern]
);
const notes: Note[] = [];
while (resultSet.goToNextRow() === true) {
notes.push(this.parseNoteFromResultSet(resultSet));
}
resultSet.close();
return notes;
}
// 更新笔记
async updateNote(note: Note): Promise<void> {
const valueBucket: relationalStore.ValuesBucket = {
title: note.title,
content: note.content,
category: note.category,
update_time: Date.now(),
is_favorite: note.isFavorite ? 1 : 0
};
const predicate = new relationalStore.RdbPredicates('notes');
predicate.equalTo('id', note.id);
await this.store!.update(valueBucket, predicate);
}
// 软删除(不是真的删除,只是标记为已删除)
async softDeleteNote(id: number): Promise<void> {
const valueBucket: relationalStore.ValuesBucket = {
is_deleted: 1,
update_time: Date.now()
};
const predicate = new relationalStore.RdbPredicates('notes');
predicate.equalTo('id', id);
await this.store!.update(valueBucket, predicate);
}
// 真删除(清理回收站时用)
async hardDeleteNote(id: number): Promise<void> {
const predicate = new relationalStore.RdbPredicates('notes');
predicate.equalTo('id', id);
await this.store!.delete(predicate);
}
// 获取笔记总数
async getNoteCount(): Promise<number> {
const resultSet = await this.store!.querySql(
`SELECT COUNT(*) as count FROM notes WHERE is_deleted = 0`
);
resultSet.goToFirstRow();
const count = resultSet.getLong(resultSet.getColumnIndex('count'));
resultSet.close();
return count;
}
private parseNoteFromResultSet(resultSet: relationalStore.ResultSet): Note {
return {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
category: resultSet.getString(resultSet.getColumnIndex('category')),
createTime: resultSet.getLong(resultSet.getColumnIndex('create_time')),
updateTime: resultSet.getLong(resultSet.getColumnIndex('update_time')),
isFavorite: resultSet.getLong(resultSet.getColumnIndex('is_favorite')) === 1
};
}
}
// 笔记数据模型
interface Note {
id?: number;
title: string;
content: string;
category: string;
createTime?: number;
updateTime?: number;
isFavorite?: boolean;
}
RDB 使用要点:
-
始终使用预编译语句(参数化查询):上面代码中的
querySql(sql, [param1, param2])就是预编译语句。永远不要拼接 SQL 字符串,否则会引入 SQL 注入风险。 -
ResultSet 用完必须 close: 否则会占用数据库连接,导致后续查询失败。我习惯在
while循环结束后立即 close。 -
软删除优于硬删除: 用户误删后可以从回收站恢复,这是用户体验的重要细节。
-
时间戳用整数存储: 不要用字符串存日期,用
Date.now()返回的毫秒整数,查询排序效率最高。
二、标准化数据定义:让数据说同一种语言
2.1 为什么需要标准化数据
想象一个场景:你的笔记应用支持"把笔记分享给微信好友"。这时候系统需要知道"这是一条文本信息",而不是一个二进制 blob。鸿蒙的 Uniform Data Type 就是干这个的——它定义了一套标准的数据类型体系,让不同应用之间知道"对方给的是什么"。
Uniform Data Type 不是存储方案,而是"数据语义描述"方案。它回答的问题是:这个数据是什么类型?一段文字、一张图片、一个地理位置、还是一个联系人?
2.2 核心概念:Uniform Data Type 与 UTD
标准化数据类型层级(部分):
openharmony.uniform-data.text ← 通用文本
├── openharmony.uniform-data.plain-text ← 纯文本
├── openharmony.uniform-data.html ← HTML 富文本
└── openharmony.uniform-data.hyperlink ← 超链接
openharmony.uniform-data.image
├── openharmony.uniform-data.jpeg
├── openharmony.uniform-data.png
└── openharmony.uniform-data.gif
openharmony.uniform-data.file
├── openharmony.uniform-data.pdf
├── openharmony.uniform-data.word
└── openharmony.uniform-data.excel
openharmony.uniform-data.contact ← 联系人信息
openharmony.uniform-data.location ← 地理位置
openharmony.uniform-data.video ← 视频
openharmony.uniform-data.audio ← 音频
2.3 实战:把笔记包装成标准数据类型进行分享
import { uniformTypeDescriptor, uniformDataStruct } from '@kit.ArkData';
class NoteDataPacker {
// 把一条笔记封装成标准化的 UniformData
static packNoteToUniformData(note: Note): uniformDataStruct.UniformData {
// 创建一个纯文本数据记录
const textRecord = new uniformDataStruct.PlainText({
textContent: `${note.title}\n\n${note.content}`,
abstract: note.title, // 摘要,用于预览
details: {
'app.note.id': note.id?.toString() || '',
'app.note.category': note.category
}
});
// 创建一个 HTML 富文本记录(用于支持富文本的应用)
const htmlContent = `
<h2>${note.title}</h2>
<p>${note.content.replace(/\n/g, '<br>')}</p>
<p style="color: #999; font-size: 12px;">
来自 我的笔记App · ${note.category}
</p>
`;
const htmlRecord = new uniformDataStruct.HTML({
htmlContent: htmlContent,
plainContent: note.content
});
// 创建 UniformData 容器,包含两种格式的记录
const uniformData = new uniformDataStruct.UniformData();
uniformData.addRecord(textRecord);
uniformData.addRecord(htmlRecord);
return uniformData;
}
// 从标准数据解析回笔记
static parseNoteFromUniformData(uniformData: uniformDataStruct.UniformData): Partial<Note> | null {
// 优先获取纯文本
const textRecords = uniformData.getRecords(uniformTypeDescriptor.UniformDataType.PLAIN_TEXT);
if (textRecords.length > 0) {
const textRecord = textRecords[0] as uniformDataStruct.PlainText;
return {
title: textRecord.abstract || '无标题',
content: textRecord.textContent || '',
category: textRecord.details?.['app.note.category'] || 'uncategorized'
};
}
// 如果没有纯文本,尝试解析 HTML
const htmlRecords = uniformData.getRecords(uniformTypeDescriptor.UniformDataType.HTML);
if (htmlRecords.length > 0) {
const htmlRecord = htmlRecords[0] as uniformDataStruct.HTML;
return {
title: 'HTML 内容',
content: htmlRecord.plainContent || htmlRecord.htmlContent || ''
};
}
return null;
}
}
// 使用:把笔记分享到系统分享面板
import { systemShare } from '@kit.ShareKit';
async function shareNote(note: Note): Promise<void> {
const uniformData = NoteDataPacker.packNoteToUniformData(note);
await systemShare.share({
data: uniformData,
title: `分享笔记:${note.title}`,
preview: note.title
});
}
标准化数据的好处:
-
跨应用识别: 系统分享面板能自动识别你的数据是"文本",可以展示给所有支持文本接收的应用。
-
格式兼容性: 你同时提供了纯文本和 HTML 两种格式,接收方应用优先选择它支持的那种。
-
扩展性:
details字段可以携带自定义的键值对,比如笔记 ID、分类等,实现"应用间数据回跳"。
三、跨应用数据共享:打破应用之间的围墙
3.1 数据共享的两种模式
鸿蒙提供了两种跨应用数据共享方案:
| 方式 | 特点 | 适用场景 |
|---|---|---|
| DataShare | 数据提供方(Provider)通过 URI 暴露数据,接收方(Consumer)通过 URI 查询 | 应用之间的结构化数据共享,如通讯录、日历、笔记 |
| 文件分享 | 通过文件 URI 或分布式文件系统共享 | 图片、文档、媒体文件等 |
3.2 DataShare 实战:让其他应用读取你的笔记
假设你的笔记应用想开放一个接口:允许其他应用(比如一个待办清单应用)读取你的笔记列表。这就是 DataShare 的典型场景。
第一步:声明数据提供方(DataShareExtensionAbility)
// module.json5 中声明
{
"module": {
"extensionAbilities": [
{
"name": "NotesDataShare",
"srcEntry": "./ets/datashare/NotesDataShare.ets",
"type": "dataShare",
"exported": true,
"uri": "datashare://com.example.notesapp/notes"
}
]
}
}
第二步:实现 DataShareExtensionAbility
import { DataShareExtensionAbility } from '@kit.AbilityKit';
import { relationalStore } from '@kit.ArkData';
export default class NotesDataShare extends DataShareExtensionAbility {
private store: relationalStore.RdbStore | null = null;
async onCreate(want, callback) {
// 初始化数据库连接
this.store = await relationalStore.getRdbStore(this.context, {
name: 'NotesDB.db',
securityLevel: relationalStore.SecurityLevel.S1
});
callback();
}
// 查询接口:其他应用可以通过 URI 调用这个来查数据
async query(uri: string, predicates, columns, callback) {
if (!this.store) {
callback(null);
return;
}
// 解析查询条件
const rdbPredicates = new relationalStore.RdbPredicates('notes');
rdbPredicates.equalTo('is_deleted', 0);
// 如果传了分类过滤条件
if (predicates?.category) {
rdbPredicates.equalTo('category', predicates.category);
}
// 执行查询
const resultSet = await this.store.query(rdbPredicates, columns || ['id', 'title', 'content', 'category', 'update_time']);
callback(resultSet);
}
// 插入接口(其他应用可以向你这里写入数据)
async insert(uri: string, value, callback) {
if (!this.store) {
callback(-1);
return;
}
const result = await this.store.insert('notes', value);
callback(result);
}
// 更新接口
async update(uri: string, predicates, value, callback) {
if (!this.store) {
callback(-1);
return;
}
const rdbPredicates = new relationalStore.RdbPredicates('notes');
// 根据 predicates 构建条件...
const result = await this.store.update(value, rdbPredicates);
callback(result);
}
// 删除接口
async delete(uri: string, predicates, callback) {
if (!this.store) {
callback(-1);
return;
}
const rdbPredicates = new relationalStore.RdbPredicates('notes');
// 根据 predicates 构建条件...
const result = await this.store.delete(rdbPredicates);
callback(result);
}
}
第三步:其他应用(Consumer)读取这个数据
import { dataShare } from '@kit.ArkData';
class NotesReader {
private helper: dataShare.DataShareHelper | null = null;
private uri = 'datashare://com.example.notesapp/notes';
async connect(context: Context): Promise<void> {
this.helper = await dataShare.createDataShareHelper(context, this.uri);
}
// 读取笔记应用里的笔记列表
async fetchNotes(): Promise<any[]> {
if (!this.helper) return [];
// 查询条件:只读取"工作"分类的笔记
const predicates = {
category: '工作'
};
const resultSet = await this.helper.query(this.uri, predicates, ['id', 'title', 'content', 'category']);
const notes: any[] = [];
while (resultSet.goToNextRow() === true) {
notes.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content'))
});
}
resultSet.close();
return notes;
}
// 向笔记应用里写入一条记录
async addNoteToOtherApp(title: string, content: string): Promise<number> {
if (!this.helper) return -1;
const valueBucket = {
title: title,
content: content,
category: 'imported',
create_time: Date.now(),
update_time: Date.now()
};
return await this.helper.insert(this.uri, valueBucket);
}
}
DataShare 的关键点:
-
URI 是身份标识:
datashare://com.example.notesapp/notes这个 URI 就是数据提供方的"地址"。其他应用通过它找到你的数据。 -
权限控制: 数据提供方需要在
module.json5里声明exported: true,但这不意味着谁都能访问。系统级别的权限控制仍然生效,接收方需要申请相应权限。 -
DataShare 共享的是"数据访问能力",不是数据副本。 其他应用查询的是你的数据库,数据仍然存储在你的应用沙箱里。
3.3 分布式数据共享:跨设备的数据同步
鸿蒙的分布式数据能力是它区别于其他平台的独特优势。你的笔记在手机上写了一半,走到平板前继续编辑——数据已经自动同步过去了。
import { distributedDataObject } from '@kit.ArkData';
// 创建一个分布式数据对象(跨设备同步)
class DistributedNote {
// 被 @Observed 修饰的属性会自动跨设备同步
@Observed title: string = '';
@Observed content: string = '';
@Observed category: string = 'uncategorized';
@Observed updateTime: number = Date.now();
// 非分布式属性(只在本地)
localDraft: string = ''; // 未保存的草稿,不同步
}
async function setupDistributedNote(context: Context): Promise<void> {
// 创建分布式数据对象
const note = new DistributedNote();
note.title = '跨设备笔记';
note.content = '我在手机上写的内容';
const distributedObject = distributedDataObject.create(context, note);
// 设置设备同步范围
await distributedObject.setSessionId('note_session_001');
// 保存到分布式数据库
await distributedObject.save('notes');
console.info('笔记已保存到分布式数据库,其他设备可以读取');
}
// 在另一台设备上读取
async function readDistributedNote(context: Context): Promise<void> {
const distributedObject = distributedDataObject.create(context, new DistributedNote());
await distributedObject.setSessionId('note_session_001');
await distributedObject.restore('notes');
// 此时 note 的数据已经包含了其他设备上写入的内容
const note = distributedObject.getData() as DistributedNote;
console.info(`同步后的笔记:${note.title} - ${note.content}`);
}
分布式数据的限制:
- 只能同步简单数据类型(string、number、boolean、对象),不能同步文件或二进制大对象。
- 同步有延迟,不适合实时性要求极高的场景(如在线协作编辑)。
- 设备需要在同一个华为账号下,且都开启了分布式能力。
四、数据可靠性与安全性:数据不丢、不被偷
4.1 数据库加密:敏感数据必须加密
如果你的应用存储了用户的身份证号、银行卡号、密码等敏感信息,必须用加密数据库。RDB 支持内置加密,只需要在配置时开启。
import { relationalStore } from '@kit.ArkData';
// 加密数据库配置
const ENCRYPTED_DB_CONFIG: relationalStore.StoreConfig = {
name: 'SecureNotesDB.db',
securityLevel: relationalStore.SecurityLevel.S3, // 更高的安全级别
encrypt: true // 开启加密
};
// 创建加密数据库时,系统会自动生成密钥并存储在系统的密钥管理服务中
// 你不需要自己管理密钥
async function createEncryptedDatabase(context: Context): Promise<relationalStore.RdbStore> {
const store = await relationalStore.getRdbStore(context, ENCRYPTED_DB_CONFIG);
return store;
}
// 注意:加密数据库的性能会比非加密数据库略低(约 5-10%),所以只把敏感数据表放在加密库里
// 普通数据(如用户设置、缓存)可以继续用非加密数据库
4.2 数据备份与恢复:用户换手机怎么办
鸿蒙提供了云备份能力,但你的应用需要主动声明哪些数据需要备份。
// module.json5 中配置备份策略
{
"module": {
"backup": {
"include": [
"data/preferences/app_settings",
"data/databases/NotesDB.db"
],
"exclude": [
"cache/",
"temp/"
]
}
}
}
备份策略建议:
| 数据类型 | 是否备份 | 原因 |
|---|---|---|
| 用户配置(Preferences) | 是 | 用户换了新手机,应该保留他的偏好设置 |
| 核心数据库(RDB) | 是 | 用户的笔记、记录等核心数据必须保留 |
| 缓存文件 | 否 | 缓存可以重建,不需要占用云空间 |
| 临时下载 | 否 | 临时数据,不需要备份 |
| 大文件(图片、视频) | 否 | 占用云空间太大,建议用户自己手动迁移或单独使用云存储 |
4.3 权限最小化:不要索要不必要的权限
数据安全不仅是"防止别人偷你的数据",也是"不要过度收集用户的数据"。鸿蒙的权限管控非常严格,而且应用商店审核会重点检查。
// 错误的示范:一上来就申请所有权限
// ❌ 不要这样做
// requestPermissions(['ohos.permission.CAMERA', 'ohos.permission.LOCATION',
// 'ohos.permission.MICROPHONE', 'ohos.permission.READ_MEDIA']);
// 正确的做法:按需申请
async function requestCameraWhenNeeded(): Promise<void> {
// 只有当用户点击"拍照上传笔记配图"时才申请相机权限
const authStatus = await abilityAccessCtrl.createAtManager()
.checkAccessToken(tokenID, 'ohos.permission.CAMERA');
if (authStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
const result = await abilityAccessCtrl.createAtManager()
.requestPermissionsFromUser(getContext(), ['ohos.permission.CAMERA']);
if (result.authResults[0] !== 0) {
// 用户拒绝了,友好提示
AlertDialog.show({
message: '需要相机权限才能拍照添加笔记配图'
});
return;
}
}
// 用户已授权,打开相机
this.openCamera();
}
4.4 数据可靠性:事务与并发控制
当多个操作同时访问数据库时,必须使用事务保证数据一致性。
class TransactionManager {
private store: relationalStore.RdbStore;
constructor(store: relationalStore.RdbStore) {
this.store = store;
}
// 批量插入笔记(带事务)
async batchInsertNotes(notes: Note[]): Promise<void> {
// 开始事务
await this.store.beginTransaction();
try {
for (const note of notes) {
const valueBucket = {
title: note.title,
content: note.content,
category: note.category,
create_time: Date.now(),
update_time: Date.now()
};
await this.store.insert('notes', valueBucket);
}
// 全部成功,提交事务
await this.store.commit();
console.info(`成功插入 ${notes.length} 条笔记`);
} catch (e) {
// 任何一步出错,回滚所有操作
await this.store.rollBack();
console.error('批量插入失败,已回滚:', e);
throw e;
}
}
}
事务的关键点:
-
要么全成功,要么全失败。 如果你批量导入 100 条笔记,第 50 条出错了,前面 49 条也不会被写入——这是事务的原子性保证。
-
事务期间数据库会被锁定。 不要在事务里做耗时的操作(如网络请求、文件读写),否则会导致数据库长时间被锁,影响其他操作。
-
单条操作不需要事务。 只有多条操作之间存在依赖关系时才需要事务。
五、实战整合:一个笔记应用的完整数据层设计
把前面讲的所有能力串起来,下面是笔记应用的完整数据层架构。
数据层架构
├── 配置层(Preferences)
│ ├── 暗色模式开关
│ ├── 字体大小
│ └── 用户登录状态
│
├── 业务层(RDB)
│ ├── notes 表(笔记主表)
│ ├── categories 表(分类表)
│ └── attachments 表(附件表)
│
├── 共享层(DataShare)
│ └── 暴露给其他应用的查询/写入接口
│
├── 标准层(Uniform Data Type)
│ └── 笔记导出为 PlainText / HTML 格式
│
└── 安全层
├── 加密数据库(敏感附件)
├── 云备份配置
└── 权限最小化
完整的 NoteRepository 封装:
class NoteRepository {
private db: NoteDatabase;
private config: AppConfig;
constructor(context: Context) {
this.init(context);
}
private async init(context: Context): Promise<void> {
this.db = await NoteDatabase.getInstance(context);
this.config = await AppConfig.getInstance(context);
}
// 保存笔记(同时更新本地数据库和分布式数据)
async saveNote(note: Note): Promise<void> {
if (note.id) {
await this.db.updateNote(note);
} else {
const id = await this.db.insertNote(note);
note.id = id;
}
// 同步到分布式数据(如果用户开启了多设备同步)
const syncEnabled = await this.config.getBoolean('sync_enabled', false);
if (syncEnabled) {
await this.syncToDistributed(note);
}
}
// 搜索笔记(同时搜索标题和内容)
async searchNotes(query: string): Promise<Note[]> {
if (!query.trim()) {
return this.db.queryAllNotes();
}
return this.db.searchNotes(query.trim());
}
// 导出笔记为标准化数据(用于分享)
exportNote(note: Note): uniformDataStruct.UniformData {
return NoteDataPacker.packNoteToUniformData(note);
}
// 从其他应用导入笔记
async importFromUniformData(data: uniformDataStruct.UniformData): Promise<Note | null> {
const parsed = NoteDataPacker.parseNoteFromUniformData(data);
if (!parsed) return null;
const note: Note = {
title: parsed.title || '导入的笔记',
content: parsed.content || '',
category: 'imported',
createTime: Date.now(),
updateTime: Date.now()
};
const id = await this.db.insertNote(note);
note.id = id;
return note;
}
// 同步到分布式数据(跨设备)
private async syncToDistributed(note: Note): Promise<void> {
// 实际实现参考前面的分布式数据代码
}
}
六、总结:选择正确工具的决策树
面对 ArkData 这么多能力,怎么选?我用一个决策树帮你理清:
你的数据是什么?
├── 简单的配置项(开关、偏好、用户设置)
│ └── 用 Preferences(键值对)
│
├── 结构化的业务数据(笔记、记录、列表)
│ └── 用 RDB(关系型数据库)
│ ├── 数据量 < 1000 条、简单查询
│ │ └── 单表 + 基础 SQL 足够
│ ├── 数据量 > 1000 条、复杂查询
│ │ └── 多表设计 + 索引优化
│ └── 包含敏感信息
│ └── 开启 encrypt: true(加密数据库)
│
├── 需要分享给其他应用
│ └── 用 Uniform Data Type(标准化数据定义)
│ └── 配合 DataShare 暴露数据接口
│
├── 需要跨设备同步
│ └── 用 Distributed Data Object(分布式数据对象)
│ └── 注意:仅支持简单数据类型
│
└── 需要备份到云端
└── 在 module.json5 中配置 backup 策略
└── 核心数据 include,缓存数据 exclude
最后一点建议: 不要为了追求"用更高级的方案"而过度设计。你的第一款应用,Preferences 存配置、RDB 存业务数据,就够了。等真的遇到"需要分享给其他应用"或"需要跨设备同步"的场景时,再引入 DataShare 和分布式数据。代码是写来解决问题的,不是写来展示技术栈的。
如果这篇文章对你有帮助,建议收藏。数据层是应用的地基,地基打好了,上面的功能才能稳。
更多推荐



所有评论(0)