华为鸿蒙 ArkTS 实战:基于 RelationalStore 的 SQLite 实现本地数据持久化

点这里 -> 加入鸿蒙官方班级:


活动详情 / 激励点击这里查看:https://developer.huawei.com/consumer/cn/activity/201753759057944811

文章目录

      • 一、前置准备
          • [1. 依赖准备](#1. 依赖准备)
          • [2. 权限申请](#2. 权限申请)
      • [二、核心代码解析:DB 工具类的设计与实现](#二、核心代码解析:DB 工具类的设计与实现)
          • [1. 类结构与静态属性](#1. 类结构与静态属性)
          • [2. 数据库初始化:init () 方法](#2. 数据库初始化:init () 方法)
          • [3. 数据插入:insert_data () 方法](#3. 数据插入:insert_data () 方法)
          • [4. 数据查询:query_data () 方法](#4. 数据查询:query_data () 方法)
          • [5. 数据更新:up_data () 方法](#5. 数据更新:up_data () 方法)
          • [6. 数据删除:del () 方法](#6. 数据删除:del () 方法)
      • [三、实战调用:在鸿蒙页面中使用 DB 工具类](#三、实战调用:在鸿蒙页面中使用 DB 工具类)
          • [1. 页面结构(ArkTS 声明式 UI)](#1. 页面结构(ArkTS 声明式 UI))
          • [2. 调用流程解析](#2. 调用流程解析)
      • 四、常见问题与注意事项
          • [1. 上下文获取失败:](#1. 上下文获取失败:)
          • [2. 字段名不匹配:](#2. 字段名不匹配:)
          • [3. 异步方法未处理错误:](#3. 异步方法未处理错误:)
          • [4. 密码明文存储:](#4. 密码明文存储:)
          • [5. 数据库版本升级:](#5. 数据库版本升级:)
      • 五、总结

在鸿蒙应用开发中,本地数据持久化是高频需求 ------ 无论是存储用户配置、离线缓存还是业务数据,都需要可靠的本地存储方案。鸿蒙提供的 RelationalStore(关系型数据库)基于 SQLite 实现,兼顾了结构化数据管理的灵活性和轻量级特性。

用户首选项(Preferences)参考上期文章:华为HarmonyOS NEXT 原生应用开发: 数据持久化存储(用户首选项)的使用 token令牌存储鉴权!

一、前置准备

在开始前,确保咱们的开发环境和依赖都已配置到位,避免后续踩坑

1. 依赖准备

RelationalStore 属于鸿蒙系统基础能力,无需额外引入第三方库,但需在代码中导入相关模块。核心导入包如下(后续代码会用到):

ts 复制代码
// 基础错误处理
import { BusinessError } from '@kit.BasicServicesKit';
// 关系型数据库核心API
import relationalStore from '@ohos.data.relationalStore';
// 上下文(获取应用上下文)
import { Context } from '@ohos.ability.context';
2. 权限申请

虽然 RelationalStore 操作本地数据库无需额外的 "危险权限",但需确保应用有本地存储访问权限(默认已授予,若手动关闭需在module.json5中配置):

json 复制代码
// module.json5 -> module -> abilities -> permissions(按需添加)
"permissions": [
  {
    "name": "ohos.permission.READ_USER_STORAGE"
  },
  {
    "name": "ohos.permission.WRITE_USER_STORAGE"
  }
]

二、核心代码解析:DB 工具类的设计与实现

本文的核心是一份封装好的DB工具类,包含数据库初始化、增删改查(CRUD)全套功能。咱们逐段拆解,理解每个方法的作用和原理。

1. 类结构与静态属性

首先看DB类的整体结构,用静态属性静态方法设计,是为了避免频繁实例化,实现全局单例访问(数据库实例全局唯一即可):

ts 复制代码
export class DB {  
  // 应用上下文(必须赋值,否则无法初始化数据库)
  static context: Context;
  // RdbStore实例(数据库操作的核心对象,类似SQLite的Connection)
  static jjb: relationalStore.RdbStore;

  // 后续方法...
}
  • context:鸿蒙应用的上下文,用于获取数据库存储路径、权限等信息,必须在初始化前赋值(后面实战会讲如何赋值)。
  • jjb:RdbStore是 RelationalStore 的核心类,所有数据库操作(建表、增删改查)都通过它完成,这里命名为jjb(可自定义,建议改为rdbStore更直观)。
2. 数据库初始化:init () 方法

初始化是第一步 ------ 创建数据库文件、创建数据表,代码如下:

ts 复制代码
// 初始化数据库
static init() {
  // 调用getRdbStore创建/打开数据库
  relationalStore.getRdbStore(
    // 上下文(若未赋值,用getContext()获取默认上下文)
    DB.context || getContext(),
    // 数据库配置项
    {
      name: 'mdb.db', // 数据库文件名(存储路径:/data/data/应用包名/files/mdb.db)
      securityLevel: relationalStore.SecurityLevel.S1 // 安全级别(S1:普通加密,适合一般数据)
    },
    // 回调函数(成功返回RdbStore,失败返回错误)
    (err: BusinessError, rdbStore) => {
      if (err) {
        console.log("数据库创建失败!错误信息:" + err.message);
        return;
      }
      console.log("数据库创建成功!");
      // 保存RdbStore实例到静态属性,供后续使用
      DB.jjb = rdbStore;
      // 执行SQL:创建TK表(若不存在)
      rdbStore.executeSql(
        `CREATE TABLE IF NOT EXISTS TK (
          ID INTEGER PRIMARY KEY,  // 主键(自增可选,这里未设置AUTOINCREMENT)
          NAME TEXT NOT NULL,      // 用户名(非空)
          PASSWORD TEXT NOT NULL   // 密码(非空,实际开发需加密存储!)
        )`
      );
    }
  );
}

关键解析:

  • getRdbStore:鸿蒙提供的创建 / 打开数据库的 API,异步执行(通过回调返回结果)。若数据库已存在,则直接打开;若不存在,则创建后打开。
  • 安全级别SecurityLevel.S1是默认的普通安全级别,数据会进行基础加密;若需更高安全级(如敏感数据),可使用S2(需额外配置密钥)。
  • 建表 SQL :(统一为NAME、PASSWORD),SQLite 虽默认不区分字段名大小写,但统一命名可避免后续查询 / 更新时的字段匹配问题。
  • 注意点 :原代码中ID未设置AUTOINCREMENT,若需主键自增,需修改为ID INTEGER PRIMARY KEY AUTOINCREMENT
3. 数据插入:insert_data () 方法

向TK表插入一条用户数据,核心是ValuesBucket(鸿蒙封装的键值对容器,对应表的字段和值):

ts 复制代码
// 插入数据(id:主键,name:用户名,password:密码)
static insert_data(id_1: number, name_1: string, password_1: string) {
  // 1. 构建键值对(键必须与表字段一致!)
  const task: relationalStore.ValuesBucket = {
    ID: id_1,       // 对应表的ID字段
    NAME: name_1,   // 对应表的NAME字段
    PASSWORD: password_1 // 对应表的PASSWORD字段
  };

  // 2. 调用insert方法插入数据(异步,用then/catch处理结果)
  DB.jjb.insert("TK", task)
    .then((rowId: number) => {
      console.info("数据插入成功!插入行ID:" + rowId);
    })
    .catch((err: BusinessError) => {
      console.error("数据插入失败!错误信息:" + err.message);
    });
}

关键解析:

  • ValuesBucket :类似 JSON 对象,但值类型需与表字段类型匹配(如ID是INTEGER,则值必须是数字)。原代码中键用id、name(小写),与表字段ID、NAME(大写)不匹配,会导致插入失败,这里已修正。
  • insert 返回值rowId是插入数据的行 ID(与ID字段不同,rowId是 SQLite 自动维护的行标识),可用于后续验证插入结果。
  • 安全提示 :实际开发中,密码不能明文存储!需用鸿蒙的cryptoFramework(加密框架)加密后再插入,避免数据泄露。
4. 数据查询:query_data () 方法

查询TK表的所有数据,用RdbPredicates设置查询条件(这里查询所有),用async/await处理异步查询结果:

ts 复制代码
// 查询数据(异步,用async/await简化异步逻辑)
static async query_data() {
  // 1. 创建查询条件(RdbPredicates:类似SQL的WHERE子句)
  // 这里不设条件,查询TK表所有数据
  let pre = new relationalStore.RdbPredicates("TK");

  // 2. 执行查询(指定返回的字段:ID、NAME、PASSWORD)
  let result = await DB.jjb.query(pre, ['ID', 'NAME', 'PASSWORD']);

  // 3. 遍历查询结果(result是ResultSet对象,需逐行读取)
  try {
    // 判断是否有数据
    if (result.rowCount === 0) {
      console.info("查询结果为空!");
      return;
    }

    // 逐行读取数据(isAtLastRow():是否到最后一行)
    while (!result.isAtLastRow()) {
      // 移动到下一行(首次需调用,否则读不到第一行)
      result.goToNextRow();

      // 获取字段值(getColumnIndex:通过字段名获取索引,避免硬编码索引)
      let id = result.getLong(result.getColumnIndex('ID')); // INTEGER类型用getLong
      let name = result.getString(result.getColumnIndex('NAME')); // TEXT类型用getString
      let password = result.getString(result.getColumnIndex('PASSWORD'));

      console.info(`查询到数据:ID=${id}, NAME=${name}, PASSWORD=${password}`);
    }
  } catch (err) {
    console.error("查询数据失败!错误信息:" + (err as BusinessError).message);
  } finally {
    // 4. 关闭ResultSet(必须关闭,避免内存泄漏)
    result.close();
  }
}

关键解析:

  • RdbPredicates :若需条件查询(如查询ID=1的用户),可添加pre.equalTo('ID', 1);若需模糊查询,可使用pre.like('NAME', '%张%')
  • ResultSet 遍历rowCount获取总记录数,goToNextRow()移动到下一行,getColumnIndex()通过字段名获取索引(比硬编码索引更易维护)。
  • 资源释放:result.close()必须调用,否则会导致内存泄漏(原代码未关闭,这里补充)。
5. 数据更新:up_data () 方法

根据ID更新TK表中的数据,核心是 "更新条件" 和 "更新内容":

ts 复制代码
// 更新数据(a:要更新的键值对,id:更新条件(ID))
static up_data(a: relationalStore.ValuesBucket, id: number) {
  // 1. 要更新的数据(键必须与表字段一致)
  let task: relationalStore.ValuesBucket = a;

  // 2. 设置更新条件:只更新ID=id的行
  let result = new relationalStore.RdbPredicates("TK");
  result.equalTo('ID', id); // 条件:ID等于指定值

  // 3. 执行更新(异步,用then/catch处理结果)
  DB.jjb.update(task, result)
    .then((rowCount: number) => {
      if (rowCount === 0) {
        console.info("未找到要更新的数据(ID不存在)!");
        return;
      }
      console.info("数据更新成功!更新行数:" + rowCount);
    })
    .catch((err: BusinessError) => {
      console.error("数据更新失败!错误信息:" + err.message);
    });
}

关键解析

  • 更新条件equalTo('ID', id)确保只更新指定ID的行,避免批量更新错误(若不设条件,会更新表中所有数据!)。
  • 返回值: rowCount是更新的行数,若为 0,说明没有符合条件的数据(如ID不存在),原代码中错误打印 "数据插入成功",这里已修正为 "数据更新成功"。
6. 数据删除:del () 方法

根据ID删除TK表中的数据,逻辑与更新类似,核心是 "删除条件":

ts 复制代码
// 删除数据(id:删除条件(ID))
static del(id: number) {
  // 1. 设置删除条件:只删除ID=id的行
  let predicates = new relationalStore.RdbPredicates("TK");
  predicates.equalTo('ID', id);

  // 2. 执行删除(异步,用then/catch处理结果)
  DB.jjb.delete(predicates)
    .then((rowCount: number) => {
      if (rowCount === 0) {
        console.info("未找到要删除的数据(ID不存在)!");
        return;
      }
      console.info("数据删除成功!删除行数:" + rowCount);
    })
    .catch((err: BusinessError) => {
      console.error("数据删除失败!错误信息:" + err.message);
    });
}

关键解析:

  • 删除安全:务必设置明确的删除条件(如ID=id),若不设条件,会删除表中所有数据,风险极高!
  • 结果验证rowCount为 0 时,说明要删除的ID不存在,需给用户明确提示(如 "该用户不存在")。

三、实战调用:在鸿蒙页面中使用 DB 工具类

代码封装好后,如何在实际页面中调用?以一个简单的 "用户数据管理" 页面为例,演示完整流程:

1. 页面结构(ArkTS 声明式 UI)
ts 复制代码
import { DB } from '../utils/DB'; // 导入DB工具类(路径根据实际项目调整)
import { AbilityContext } from '@ohos.ability.context';

@Entry
@Component
struct DataStoragePage {
  // 输入框状态
  @State inputId: string = '';
  @State inputName: string = '';
  @State inputPwd: string = '';

  // 页面加载时初始化数据库
  aboutToAppear() {
    // 1. 获取应用上下文(AbilityContext)并赋值给DB
    const context = getContext(this) as AbilityContext;
    DB.context = context;

    // 2. 初始化数据库
    DB.init();
  }

  build() {
    Column({ space: 20 }) {
      // 输入区域
      TextInput({ placeholder: '请输入ID' })
        .width('80%')
        .onChange((value) => this.inputId = value);

      TextInput({ placeholder: '请输入用户名' })
        .width('80%')
        .onChange((value) => this.inputName = value);

      TextInput({ placeholder: '请输入密码', type: InputType.Password })
        .width('80%')
        .onChange((value) => this.inputPwd = value);

      // 操作按钮
      Row({ space: 15 }) {
        Button('插入数据')
          .onClick(() => {
            // 转换ID为数字(TextInput输入是字符串)
            const id = parseInt(this.inputId);
            if (isNaN(id) || !this.inputName || !this.inputPwd) {
              console.info("输入不完整!ID必须是数字,用户名和密码不能为空");
              return;
            }
            // 调用插入方法
            DB.insert_data(id, this.inputName, this.inputPwd);
          });

        Button('查询数据')
          .onClick(() => {
            // 调用查询方法
            DB.query_data();
          });

        Button('更新数据')
          .onClick(() => {
            const id = parseInt(this.inputId);
            if (isNaN(id) || (!this.inputName && !this.inputPwd)) {
              console.info("输入不完整!ID必须是数字,至少输入用户名或密码");
              return;
            }
            // 构建要更新的数据(只更新输入的字段)
            const updateData: relationalStore.ValuesBucket = {};
            if (this.inputName) updateData.NAME = this.inputName;
            if (this.inputPwd) updateData.PASSWORD = this.inputPwd;
            // 调用更新方法
            DB.up_data(updateData, id);
          });

        Button('删除数据')
          .onClick(() => {
            const id = parseInt(this.inputId);
            if (isNaN(id)) {
              console.info("ID必须是数字!");
              return;
            }
            // 调用删除方法
            DB.del(id);
          });
      }
    }
    .width('100%')
    .padding(20)
  }
}
2. 调用流程解析
  1. 上下文赋值 :在aboutToAppear()(页面加载前)中,通过getContext(this)获取AbilityContext,赋值给DB.context------ 这是数据库初始化的前提,否则getRdbStore会失败。
  2. 初始化数据库 :调用DB.init(),创建mdb.db数据库和TK表。
  3. 按钮交互
    • 插入:验证输入(ID 为数字、用户名 / 密码非空),调用insert_data()
    • 查询:直接调用query_data(),控制台打印所有数据。
    • 更新:只更新输入的字段(如只改密码),调用up_data()
    • 删除:验证 ID 为数字,调用del()

四、常见问题与注意事项

1. 上下文获取失败:
  • 若在Component中获取不到context,可在Ability的onCreate()中赋值DB.context = this.context,再在页面中直接调用DB.init()。
  • 避免在build()方法中获取context(build()会频繁执行,可能导致重复赋值)。
2. 字段名不匹配:
  • 原代码中insert的键(id)与表字段(ID)大小写不一致,会导致插入后查询不到数据 ------ 务必统一字段名的大小写(推荐全大写或全小写)。
3. 异步方法未处理错误:
  • 代码中up_data()、del()未处理catch,若操作失败(如 ID 不存在),控制台无错误信息 ------ 务必添加then/catch,便于调试。
4. 密码明文存储:
  • 示例中密码明文存储,实际开发需用cryptoFramework加密(如 AES 加密),或使用鸿蒙的CredentialManager存储敏感信息。
5. 数据库版本升级:
  • 若后续需修改表结构(如新增AGE字段),需在getRdbStore的配置中添加version和upgrade回调,处理版本升级逻辑(避免旧表结构导致的错误)

五、总结

本文通过一份封装好的DB工具类,详细讲解了鸿蒙 ArkTS 中基于 RelationalStore 的 SQLite 本地数据持久化实现 ------ 从环境准备、代码解析到实战调用,覆盖了增删改查的全流程。
核心要点是

  1. 用静态类封装数据库操作,实现全局单例访问。
  2. 重视上下文赋值、字段名匹配、异步错误处理这三个关键细节。
  3. 实际开发中需注意数据安全(如密码加密)和资源释放(如关闭ResultSet)。

如果大家在实践中遇到问题(如数据库初始化失败、数据查不到),可以在评论区留言,咱们一起交流解决!也欢迎分享你的优化方案(如添加数据库版本管理、封装更通用的查询方法)。

欢迎大家加入鸿蒙班级、与鸿蒙同行,汇聚满天星:

点这里 -> 加入鸿蒙官方班级

相关推荐
王维志3 小时前
在Unity中使用SQLite(Sqlite-net-pcl)
unity·sqlite·c#·.net
歪歪1003 小时前
介绍一下SQLite的基本语法和常用命令
数据库·sql·架构·sqlite
kobe_OKOK_3 小时前
有一次django开发实录
数据库·django·sqlite
重回19814 小时前
Python 操作 SQLite:Peewee ORM 与传统 sqlite3.connect 的全方位对比
数据库·oracle·sqlite
lph0094 小时前
Android compose Room Sqlite 应用 (注入式)
android·数据库·sqlite
大可门耳4 小时前
Qt读写SQLite示例
jvm·qt·sqlite
程序员潘Sir8 小时前
鸿蒙应用开发从入门到实战(十三):ArkUI组件Slider&Progress
harmonyos·鸿蒙
程序员潘Sir1 天前
鸿蒙应用开发从入门到实战(十二):ArkUI组件Button&Toggle
harmonyos·鸿蒙
程序员潘Sir2 天前
鸿蒙应用开发从入门到实战(十一):ArkUI组件Text&TextInput
harmonyos·鸿蒙