在这里插入图片描述

适用版本: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}`);
}

几个重要细节:

  1. 单例模式: Preferences 应该作为单例使用,不要每次读写都创建新实例,否则会有性能问题和数据不一致风险。

  2. flush 必须调用: put() 只是把数据放到内存缓存里,必须调用 flush() 才会真正写入磁盘。很多新人忘记这一步,以为数据保存了,结果应用重启后数据丢了。

  3. 不要存大量数据: 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 使用要点:

  1. 始终使用预编译语句(参数化查询):上面代码中的 querySql(sql, [param1, param2]) 就是预编译语句。永远不要拼接 SQL 字符串,否则会引入 SQL 注入风险。

  2. ResultSet 用完必须 close: 否则会占用数据库连接,导致后续查询失败。我习惯在 while 循环结束后立即 close。

  3. 软删除优于硬删除: 用户误删后可以从回收站恢复,这是用户体验的重要细节。

  4. 时间戳用整数存储: 不要用字符串存日期,用 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
  });
}

标准化数据的好处:

  1. 跨应用识别: 系统分享面板能自动识别你的数据是"文本",可以展示给所有支持文本接收的应用。

  2. 格式兼容性: 你同时提供了纯文本和 HTML 两种格式,接收方应用优先选择它支持的那种。

  3. 扩展性: 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 的关键点:

  1. URI 是身份标识: datashare://com.example.notesapp/notes 这个 URI 就是数据提供方的"地址"。其他应用通过它找到你的数据。

  2. 权限控制: 数据提供方需要在 module.json5 里声明 exported: true,但这不意味着谁都能访问。系统级别的权限控制仍然生效,接收方需要申请相应权限。

  3. 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;
    }
  }
}

事务的关键点:

  1. 要么全成功,要么全失败。 如果你批量导入 100 条笔记,第 50 条出错了,前面 49 条也不会被写入——这是事务的原子性保证。

  2. 事务期间数据库会被锁定。 不要在事务里做耗时的操作(如网络请求、文件读写),否则会导致数据库长时间被锁,影响其他操作。

  3. 单条操作不需要事务。 只有多条操作之间存在依赖关系时才需要事务。


五、实战整合:一个笔记应用的完整数据层设计

把前面讲的所有能力串起来,下面是笔记应用的完整数据层架构。

数据层架构
├── 配置层(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 和分布式数据。代码是写来解决问题的,不是写来展示技术栈的。


如果这篇文章对你有帮助,建议收藏。数据层是应用的地基,地基打好了,上面的功能才能稳。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐