鸿蒙 韶非 UI 系列:关系数据库 @ohos.data.relationalStore,鸿蒙 SQLite 封装,结构化数据存取入门
写在前面
如果你写过鸿蒙 ArkUI 应用,大概率遇到过这个场景:
你写了个记账应用,每笔账有「金额/类型/时间/备注」四个字段。你想「用文件存 JSON 数组」——结果改一笔账要读全表、改、写全表,10 笔账卡得飞起。
你想「用 Preferences 存」——Preferences 是键值对,不能按条件查「上个月吃饭花了多少」。
你查文档发现「鸿蒙有 relationalStore,是 SQLite 封装」——你点进去发现getRdbStore拿实例、executeSql执行 DDL、insert(table, ValuesBucket)插数据、querySql返回ResultSet遍历、beginTransaction/commit/rollBack管事务——API 一脸懵。
这是「键值对存」和「关系型存」的分水岭。鸿蒙给的结构化存取答案是 @ohos.data.relationalStore——getRdbStore 拿数据库实例、executeSql 执行建表/DDL、insert/ValuesBucket 安全插入、querySql/ResultSet 查询遍历、beginTransaction/commit/rollBack 事务保证原子性。
本文就用一个真机可跑的「建库 + 建表 + 插入 + 查询 + 事务」demo,把关系数据库从「听名字一脸懵」讲到「下个项目直接抄」。代码托管在 AtomGit,文末有链接,真机实拍截图作证。这是韶非 UI 系列第五篇,接续上四篇 HTTP 网络栈 + 文件 IO + 能力调用 + 后台任务。
适合人群:写过鸿蒙应用、被「记账应用用啥存」折磨过的同学。
不适合人群:还在学@State的同学——出门左转看我的入门篇。
一、先讲清楚:关系数据库到底是啥
一句话:关系数据库是鸿蒙给应用存「结构化数据」的原生机制,管「建库 + 建表 + 增删改查 + 事务」全流程。
你之前写前端 localStorage / IndexedDB 是浏览器宿主 API——鸿蒙不是浏览器环境,没有这种。关系数据库是鸿蒙专门给结构化存取的原生机制,底层是 SQLite,能力对标前端的「IndexedDB + SQL.js」但更精细可控。
核心 API 一览:
| API | 作用 | 一句话理解 |
|---|---|---|
relationalStore.getRdbStore(context, config) |
拿 RdbStore 实例 | 「告诉系统我要用数据库」 |
rdbStore.executeSql(sql) |
执行 DDL/原始 SQL | 「建表/索引/原始 SQL」 |
rdbStore.insert(table, ValuesBucket) |
插入数据 | 「键值对插入,防 SQL 注入」 |
rdbStore.querySql(sql) |
查询数据 | 「SELECT 返回 ResultSet」 |
rdbStore.beginTransaction/commit/rollBack |
事务管理 | 「保证原子性,要么全成要么全回滚」 |
记住这五个,往下看。
二、动手:一个建库 + 建表 + 插入 + 查询 + 事务的 demo
2.1 import + 拿 UIAbilityContext
import relationalStore from '@ohos.data.relationalStore'
import common from '@ohos.app.ability.common'
@Entry
@Component
struct Index {
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext
private rdbStore: relationalStore.RdbStore | null = null
private dbName: string = 'arkts_demo.db'
private tableName: string = 'USER'
// ...
}
三个细节:
import relationalStore from '@ohos.data.relationalStore'——relationalStore是关系数据库的入口模块context: common.UIAbilityContext——RDB 是应用沙箱内建库,需 UIAbility 上下文定位沙箱rdbStore存拿到的 RdbStore 实例,后续所有操作都走它
2.2 getRdbStore + executeSql:初始化 + 建表
async initRdb(): Promise<void> {
this.stateLog = '初始化 RDB 中...'
try {
const config: relationalStore.StoreConfig = {
name: this.dbName,
securityLevel: relationalStore.SecurityLevel.S1
}
// getRdbStore 拿 RdbStore 实例(沙箱内建库)
this.rdbStore = await relationalStore.getRdbStore(this.context, config)
// 建表 SQL(CREATE TABLE IF NOT EXISTS)
const createSql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
ts INTEGER
)
`
await this.rdbStore.executeSql(createSql)
this.stateLog = `RDB 已初始化,数据库 = ${this.dbName},表 = ${this.tableName}`
} catch (e) {
this.stateLog = `初始化失败:${e.message}`
this.rdbStore = null
}
}
getRdbStore 三个关键点:
① StoreConfig 配置:数据库名 + 安全级别
const config: relationalStore.StoreConfig = {
name: this.dbName, // 数据库文件名(沙箱内)
securityLevel: relationalStore.SecurityLevel.S1 // 安全级别
}
SecurityLevel 是鸿蒙定义的数据库安全级别:
| SecurityLevel | 含义 | 用途 |
|---|---|---|
S1 |
低级 | 公开数据(普通应用) |
S2 |
中级 | 个人数据(记账/笔记) |
S3 |
高级 | 敏感数据(隐私相册) |
S4 |
最高 | 严格机密(金融/医疗) |
S1 是默认选择。如果存用户隐私数据,选 S2/S3。
② executeSql 执行 DDL/原始 SQL
await this.rdbStore.executeSql(`CREATE TABLE IF NOT EXISTS USER (...)`)
executeSql 执行建表/索引/原始 SQL,返回 Promise<void>。注意:executeSql 不返回查询结果——查询要走 querySql。
2.3 insert + ValuesBucket:安全插入
async insertData(): Promise<void> {
if (!this.rdbStore) {
this.stateLog = '尚未初始化 RDB,无法插入'
return
}
try {
const now: number = Date.now()
// ValuesBucket 键值对插入,比 raw SQL 安全(防注入)
const bucket: relationalStore.ValuesBucket = {
'name': `user_${this.insertCount + 1}`,
'age': 18 + (this.insertCount % 30),
'ts': now
}
// insert 返回 rowId
const rowId: number = await this.rdbStore.insert(this.tableName, bucket)
this.insertCount++
this.stateLog = `第 ${this.insertCount} 次插入成功,rowId = ${rowId}`
} catch (e) {
this.stateLog = `插入失败:${e.message}`
}
}
insert 三个关键点:
① ValuesBucket 键值对插入(防 SQL 注入)
const bucket: relationalStore.ValuesBucket = {
'name': `user_${this.insertCount + 1}`, // 列名 -> 值
'age': 18 + (this.insertCount % 30),
'ts': now
}
await this.rdbStore.insert(this.tableName, bucket)
ValuesBucket 是「列名 -> 值」的键值对。鸿蒙内部用参数化查询拼接,自动防 SQL 注入。比手写 INSERT INTO ... VALUES (...) 安全得多——手写 raw SQL 拼字符串,用户输入含 ' 就炸。
② insert 返回 rowId
const rowId: number = await this.rdbStore.insert(this.tableName, bucket)
rowId 是新插入行的主键自增 ID,后续更新/删除可用它索引。
2.4 querySql + ResultSet:查询遍历
async queryData(): Promise<void> {
if (!this.rdbStore) {
this.stateLog = '尚未初始化 RDB,无法查询'
return
}
try {
// querySql 执行 SELECT,返回 ResultSet
const resultSet: relationalStore.ResultSet = await this.rdbStore.querySql(
`SELECT id, name, age, ts FROM ${this.tableName} ORDER BY id DESC LIMIT 5`
)
const rows: string[] = []
// ResultSet 遍历: goToFirstRow / goToNextRow / getColumnIndex / getString/getLong
for (let i = 0; resultSet.goToNextRow(); i++) {
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'))
const name: string = resultSet.getString(resultSet.getColumnIndex('name'))
const age: number = resultSet.getLong(resultSet.getColumnIndex('age'))
rows.push(`id=${id}, name=${name}, age=${age}`)
}
// resultSet 用完必须 close 释放
resultSet.close()
this.queryCount++
this.lastQueryResult = rows.length > 0 ? rows.join('\n') : '(表为空)'
this.stateLog = `第 ${this.queryCount} 次查询成功,返回 ${rows.length} 行`
} catch (e) {
this.stateLog = `查询失败:${e.message}`
}
}
querySql + ResultSet 三个关键点:
① querySql 执行 SELECT 返回 ResultSet
const resultSet = await this.rdbStore.querySql(`SELECT ... FROM ...`)
ResultSet 是游标,不是数组——初始指向「第一行之前」,要主动 goToNextRow 推进。
② ResultSet 遍历:游标推进 + 列索引取值
for (let i = 0; resultSet.goToNextRow(); i++) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'))
const name = resultSet.getString(resultSet.getColumnIndex('name'))
// ...
}
goToNextRow():推进游标,返回false表示遍历完getColumnIndex('列名'):拿列索引(数字)getLong/getDouble/getString(列索引):按类型取值
③ ResultSet 用完必须 close()
resultSet.close()
ResultSet 持有底层 SQLite 游标资源,不 close 会内存泄漏。这是新手最容易忘的坑。
2.5 beginTransaction/commit/rollBack:事务保证原子性
async runTransaction(): Promise<void> {
if (!this.rdbStore) {
this.stateLog = '尚未初始化 RDB,无法跑事务'
return
}
try {
// beginTransaction 开启事务
this.rdbStore.beginTransaction()
try {
const now: number = Date.now()
// 批量插入 3 行
for (let i = 0; i < 3; i++) {
const bucket: relationalStore.ValuesBucket = {
'name': `tx_user_${now}_${i}`,
'age': 20 + i,
'ts': now
}
await this.rdbStore.insert(this.tableName, bucket)
}
// commitTransaction 提交事务
this.rdbStore.commit()
this.txLog = '事务提交成功,批量插入 3 行'
this.stateLog = '事务跑完了'
} catch (innerE) {
// rollBack 回滚事务
this.rdbStore.rollBack()
this.txLog = `事务回滚:${innerE.message}`
}
} catch (e) {
this.stateLog = `事务失败:${e.message}`
}
}
事务三个关键点:
① beginTransaction 开启事务
this.rdbStore.beginTransaction()
// ← 之后所有 SQL 在同一事务内
② commit 提交 / rollBack 回滚
try {
// ... 批量 SQL ...
this.rdbStore.commit() // 全成,提交
} catch (e) {
this.rdbStore.rollBack() // 失败,回滚所有
}
③ 事务保证原子性:要么全成要么全回滚
事务最核心的价值是原子性——比如转账,扣 A 100 + 加 B 100 必须同时成,中间崩溃要么全成要么全回滚。没事务,扣 A 完崩溃,B 没加,钱凭空消失。
三、真机实拍:建库 + 插入 + 查询 + 事务全跑通
我把这个 demo 装到真机上跑(鸿蒙 6.1.1.125, API 24),依次点 ① 初始化 RDB + ② 插入数据 ×2 + ③ 查询数据 + ④ 跑事务,下面两张都是真机实拍,没有任何 P 图。
初始态:关系数据库 Demo 标题 + 状态区「尚未初始化 RDB」+ ① 初始化 RDB / ② 插入数据 / ③ 查询数据 / ④ 跑事务四按钮 + 插入次数/查询次数指标区 + 最近查询结果区 + 事务日志区 + 关键 API 说明区:

点 ① 初始化 + ② 插入 ×2 + ③ 查询 + ④ 事务后状态:状态「RDB 已初始化,数据库 = arkts_demo.db,表 = USER」+ 插入次数 2 + 查询次数 1 + 最近查询结果显示 5 行(id/name/age)+ 事务日志「事务提交成功,批量插入 3 行」:

重点看第二张:状态显示「RDB 已初始化,数据库 = arkts_demo.db,表 = USER」——
getRdbStore+executeSql真建库建表了;插入次数 2 + 最近查询结果显示 5 行——insert+querySql真增真查了;事务日志「事务提交成功,批量插入 3 行」——beginTransaction/commit真事务跑了。这是 RDB 五大 API 全跑通的真机证明。
四、relationalStore vs 前端「IndexedDB + SQL.js」:啥差异
新手最容易纠结的问题:既然前端 IndexedDB 那么标准,鸿蒙为啥要造关系数据库?
| 维度 | 前端「IndexedDB + SQL.js」 | relationalStore |
|---|---|---|
| 运行环境 | 浏览器宿主 | 鸿蒙原生运行环境 |
| 底层引擎 | IndexedDB / SQLite-WASM | SQLite 原生编译 |
| API 风格 | 异步回调 + 对象存储 | Promise + SQL 风格 |
| 安全模型 | 同源策略 | 鸿蒙沙箱 + SecurityLevel |
| 事务 API | transaction 隐式 |
beginTransaction/commit 显式 |
| 性能 | JS-WASM 桥接,慢 | 原生 SQLite,快 |
一句话决策:鸿蒙应用存结构化数据必须用 relationalStore,不能用 IndexedDB(不存在)/localStorage(容量小 + 不能条件查)。鸿蒙不是浏览器,这套原生 SQLite 封装更安全可控、性能更高。
五、常见坑(都是血泪)
| 坑 | 症状 | 解法 |
|---|---|---|
用 localStorage/IndexedDB |
编译报错「找不到」 | 鸿蒙用 relationalStore,没浏览器宿主 API |
executeSql 拼 SQL 字符串 |
SQL 注入风险 | 查询用 querySql,插入用 insert + ValuesBucket |
ResultSet 忘 close() |
游标泄漏 + 后续查询卡 | 用完务必 resultSet.close() |
事务忘 rollBack |
异常时数据不一致 | try/catch 内 catch 调 rollBack |
| 跨应用共享数据库 | 拿不到对方 RdbStore | 鸿蒙沙箱隔离,跨应用要 ohos.permission |
| SecurityLevel 选 S4 | 普通应用拿不到 S4 | S4 限严格机密应用,普通用 S1/S2 |
querySql 期望返回数组 |
编译报错类型不匹配 | 返回 ResultSet,要遍历 |
六、relationalStore 安全模型
鸿蒙关系数据库受安全约束——沙箱隔离 + SecurityLevel 分级:
| 安全机制 | 含义 |
|---|---|
| 沙箱隔离 | 数据库文件在应用沙箱内,其他应用默认访问不到 |
| SecurityLevel S1-S4 | 数据库分级,S4 最严,限严格机密应用 |
| 跨应用共享 | 要 ohos.permission 权限 + 主动 registerStoreObserver |
这是鸿蒙安全模型的硬约束——比浏览器 IndexedDB 同源策略严,但比 iOS Keychain 松(鸿蒙沙箱可控粒度更细)。
七、完整代码仓库
本文所有代码都已托管到 AtomGit,欢迎 clone、提 issue、点 star:
🔗 仓库地址:https://atomgit.com/JaneConan/arkui-rdb
仓库包含:
- 完整的「建库 + 建表 + 插入 + 查询 + 事务」demo 工程
Index.ets主页面(getRdbStore+executeSql+insert/ValuesBucket+querySql/ResultSet+beginTransaction/commit/rollBack五姿势)StoreConfig配置 +SecurityLevel分级说明- 可直接用 DevEco Studio 打开运行(真机装普通应用必能跑)
八、下一步该学什么?
跑通这个 demo 之后,你的鸿蒙结构化存取就入门了。这是韶非 UI 系列第五篇,后续按这个顺序往下:
- WebSocket
@ohos.net.webSocket(下一篇):长连接、推送、实时通讯,聊天应用必学 - 媒体访问
@ohos.file.photoAccessHelper:访问相册、扫描媒体文件,应用调系统相册必学 - 推送通知
@ohos.notificationManager:通知栏展示、点击拉起,离线触达必学 - 动画
@ohos.arkui.animation:属性动画、转场动画,UI 进阶必学 - 相机
@ohos.multimedia.camera:预览、拍照、录像,相机应用必学
写在最后
relationalStore 的本质,是**「鸿蒙给应用存结构化数据的原生 SQLite 封装」**——不是浏览器 IndexedDB,是鸿蒙专门给关系型存取的原生机制,能力对标「IndexedDB + SQL.js」但更安全可控、性能更高。代价是 ValuesBucket 键值对插入多一步、ResultSet 游标遍历多一步。
一旦你开始用关系数据库思维写结构化存取,你会发现大部分「记账应用按月查开销」「笔记应用按标签筛笔记」「待办按截止时间排」的需求,都是 getRdbStore + executeSql + insert/ValuesBucket + querySql/ResultSet 的自然结果。代码量比 localStorage 多两行,结构化查询能力高九成。
代码已经给你了,仓库链接在上面。现在关掉这篇文章,打开 DevEco Studio,把 demo 跑起来,亲手点建库建表插入查询事务五大姿势感受下结构化存取。
跑通了,回来评论区打个「1」,我看看有多少人真的动手了。🚀
作者:JaneConan
仓库:https://atomgit.com/JaneConan/arkui-rdb
协议:Apache-2.0,随便用,别告我
更多推荐



所有评论(0)