华为鸿蒙 ArkTS 实战:基于 RelationalStore 的 SQLite 实现本地数据持久化
点这里 -> 加入鸿蒙官方班级:
活动详情 / 激励点击这里查看:https://developer.huawei.com/consumer/cn/activity/201753759057944811
文章目录
- 一、前置准备
- 1. 依赖准备
- 2. 权限申请
- 二、核心代码解析:DB 工具类的设计与实现
- 1. 类结构与静态属性
- 2. 数据库初始化:init () 方法
- 3. 数据插入:insert_data () 方法
- 4. 数据查询:query_data () 方法
- 5. 数据更新:up_data () 方法
- 6. 数据删除:del () 方法
- 三、实战调用:在鸿蒙页面中使用 DB 工具类
- 1. 页面结构(ArkTS 声明式 UI)
- 2. 调用流程解析
- 四、常见问题与注意事项
- 1. 上下文获取失败:
- 2. 字段名不匹配:
- 3. 异步方法未处理错误:
- 4. 密码明文存储:
- 5. 数据库版本升级:
- 五、总结
在鸿蒙应用开发中,本地数据持久化是高频需求 —— 无论是存储用户配置、离线缓存还是业务数据,都需要可靠的本地存储方案。鸿蒙提供的 RelationalStore(关系型数据库)基于 SQLite 实现,兼顾了结构化数据管理的灵活性和轻量级特性。
用户首选项(Preferences)参考上期文章:华为HarmonyOS NEXT 原生应用开发: 数据持久化存储(用户首选项)的使用 token令牌存储鉴权!
一、前置准备
在开始前,确保咱们的开发环境和依赖都已配置到位,避免后续踩坑
1. 依赖准备
RelationalStore 属于鸿蒙系统基础能力,无需额外引入第三方库,但需在代码中导入相关模块。核心导入包如下(后续代码会用到):
// 基础错误处理
import { BusinessError } from '@kit.BasicServicesKit';
// 关系型数据库核心API
import relationalStore from '@ohos.data.relationalStore';
// 上下文(获取应用上下文)
import { Context } from '@ohos.ability.context';
2. 权限申请
虽然 RelationalStore 操作本地数据库无需额外的 “危险权限”,但需确保应用有本地存储访问权限(默认已授予,若手动关闭需在module.json5中配置):
// module.json5 -> module -> abilities -> permissions(按需添加)
"permissions": [{"name": "ohos.permission.READ_USER_STORAGE"},{"name": "ohos.permission.WRITE_USER_STORAGE"}
]
二、核心代码解析:DB 工具类的设计与实现
本文的核心是一份封装好的DB工具类,包含数据库初始化、增删改查(CRUD)全套功能。咱们逐段拆解,理解每个方法的作用和原理。
1. 类结构与静态属性
首先看DB类的整体结构,用静态属性和静态方法设计,是为了避免频繁实例化,实现全局单例访问(数据库实例全局唯一即可):
export class DB { // 应用上下文(必须赋值,否则无法初始化数据库)static context: Context;// RdbStore实例(数据库操作的核心对象,类似SQLite的Connection)static jjb: relationalStore.RdbStore;// 后续方法...
}
- context:鸿蒙应用的上下文,用于获取数据库存储路径、权限等信息,必须在初始化前赋值(后面实战会讲如何赋值)。
- jjb:RdbStore是 RelationalStore 的核心类,所有数据库操作(建表、增删改查)都通过它完成,这里命名为jjb(可自定义,建议改为rdbStore更直观)。
2. 数据库初始化:init () 方法
初始化是第一步 —— 创建数据库文件、创建数据表,代码如下:
// 初始化数据库
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
(鸿蒙封装的键值对容器,对应表的字段和值):
// 插入数据(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
处理异步查询结果:
// 查询数据(异步,用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类型用getLonglet name = result.getString(result.getColumnIndex('NAME')); // TEXT类型用getStringlet 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
表中的数据,核心是 “更新条件” 和 “更新内容”:
// 更新数据(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
表中的数据,逻辑与更新类似,核心是 “删除条件”:
// 删除数据(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)
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)并赋值给DBconst 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. 调用流程解析
- 上下文赋值:在
aboutToAppear()
(页面加载前)中,通过getContext(this)
获取AbilityContext
,赋值给DB.context—— 这是数据库初始化的前提,否则getRdbStore会失败。 - 初始化数据库:调用
DB.init()
,创建mdb.db
数据库和TK表。 - 按钮交互:
-
- 插入:验证输入(ID 为数字、用户名 / 密码非空),调用
insert_data()
。
- 插入:验证输入(ID 为数字、用户名 / 密码非空),调用
-
- 查询:直接调用
query_data()
,控制台打印所有数据。
- 查询:直接调用
-
- 更新:只更新输入的字段(如只改密码),调用
up_data()
。
- 更新:只更新输入的字段(如只改密码),调用
-
- 删除:验证 ID 为数字,调用
del()
。
- 删除:验证 ID 为数字,调用
四、常见问题与注意事项
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 本地数据持久化实现 —— 从环境准备、代码解析到实战调用,覆盖了增删改查的全流程。
核心要点是:
- 用静态类封装数据库操作,实现全局单例访问。
- 重视上下文赋值、字段名匹配、异步错误处理这三个关键细节。
- 实际开发中需注意数据安全(如密码加密)和资源释放(如关闭
ResultSet
)。
如果大家在实践中遇到问题(如数据库初始化失败、数据查不到),可以在评论区留言,咱们一起交流解决!也欢迎分享你的优化方案(如添加数据库版本管理、封装更通用的查询方法)。
欢迎大家加入鸿蒙班级、与鸿蒙同行,汇聚满天星:
点这里 -> 加入鸿蒙官方班级