鸿蒙 韶非 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

importrelationalStorefrom'@ohos.data.relationalStore'importcommonfrom'@ohos.app.ability.common'@Entry@Componentstruct Index{privatecontext:common.UIAbilityContext=getContext(this)ascommon.UIAbilityContextprivaterdbStore:relationalStore.RdbStore|null=nullprivatedbName:string='arkts_demo.db'privatetableName:string='USER'// ...}

三个细节:

  1. import relationalStore from '@ohos.data.relationalStore'——relationalStore是关系数据库的入口模块
  2. context: common.UIAbilityContext——RDB 是应用沙箱内建库,需 UIAbility 上下文定位沙箱
  3. rdbStore存拿到的 RdbStore 实例,后续所有操作都走它

2.2getRdbStore+executeSql:初始化 + 建表

asyncinitRdb():Promise<void>{this.stateLog='初始化 RDB 中...'try{constconfig:relationalStore.StoreConfig={name:this.dbName,securityLevel:relationalStore.SecurityLevel.S1}// getRdbStore 拿 RdbStore 实例(沙箱内建库)this.rdbStore=awaitrelationalStore.getRdbStore(this.context,config)// 建表 SQL(CREATE TABLE IF NOT EXISTS)constcreateSql=`CREATE TABLE IF NOT EXISTS${this.tableName}( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, ts INTEGER )`awaitthis.rdbStore.executeSql(createSql)this.stateLog=`RDB 已初始化,数据库 =${this.dbName},表 =${this.tableName}`}catch(e){this.stateLog=`初始化失败:${e.message}`this.rdbStore=null}}

getRdbStore三个关键点:

StoreConfig配置:数据库名 + 安全级别

constconfig:relationalStore.StoreConfig={name:this.dbName,// 数据库文件名(沙箱内)securityLevel:relationalStore.SecurityLevel.S1// 安全级别}

SecurityLevel是鸿蒙定义的数据库安全级别:

SecurityLevel含义用途
S1低级公开数据(普通应用)
S2中级个人数据(记账/笔记)
S3高级敏感数据(隐私相册)
S4最高严格机密(金融/医疗)

S1 是默认选择。如果存用户隐私数据,选 S2/S3。

executeSql执行 DDL/原始 SQL

awaitthis.rdbStore.executeSql(`CREATE TABLE IF NOT EXISTS USER (...)`)

executeSql执行建表/索引/原始 SQL,返回Promise<void>。注意:executeSql不返回查询结果——查询要走querySql

2.3insert+ValuesBucket:安全插入

asyncinsertData():Promise<void>{if(!this.rdbStore){this.stateLog='尚未初始化 RDB,无法插入'return}try{constnow:number=Date.now()// ValuesBucket 键值对插入,比 raw SQL 安全(防注入)constbucket:relationalStore.ValuesBucket={'name':`user_${this.insertCount+1}`,'age':18+(this.insertCount%30),'ts':now}// insert 返回 rowIdconstrowId:number=awaitthis.rdbStore.insert(this.tableName,bucket)this.insertCount++this.stateLog=`${this.insertCount}次插入成功,rowId =${rowId}`}catch(e){this.stateLog=`插入失败:${e.message}`}}

insert三个关键点:

ValuesBucket键值对插入(防 SQL 注入)

constbucket:relationalStore.ValuesBucket={'name':`user_${this.insertCount+1}`,// 列名 -> 值'age':18+(this.insertCount%30),'ts':now}awaitthis.rdbStore.insert(this.tableName,bucket)

ValuesBucket是「列名 -> 值」的键值对。鸿蒙内部用参数化查询拼接,自动防 SQL 注入。比手写INSERT INTO ... VALUES (...)安全得多——手写 raw SQL 拼字符串,用户输入含'就炸。

insert返回rowId

constrowId:number=awaitthis.rdbStore.insert(this.tableName,bucket)

rowId是新插入行的主键自增 ID,后续更新/删除可用它索引。

2.4querySql+ResultSet:查询遍历

asyncqueryData():Promise<void>{if(!this.rdbStore){this.stateLog='尚未初始化 RDB,无法查询'return}try{// querySql 执行 SELECT,返回 ResultSetconstresultSet:relationalStore.ResultSet=awaitthis.rdbStore.querySql(`SELECT id, name, age, ts FROM${this.tableName}ORDER BY id DESC LIMIT 5`)constrows:string[]=[]// ResultSet 遍历: goToFirstRow / goToNextRow / getColumnIndex / getString/getLongfor(leti=0;resultSet.goToNextRow();i++){constid:number=resultSet.getLong(resultSet.getColumnIndex('id'))constname:string=resultSet.getString(resultSet.getColumnIndex('name'))constage: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

constresultSet=awaitthis.rdbStore.querySql(`SELECT ... FROM ...`)

ResultSet是游标,不是数组——初始指向「第一行之前」,要主动goToNextRow推进。

ResultSet遍历:游标推进 + 列索引取值

for(leti=0;resultSet.goToNextRow();i++){constid=resultSet.getLong(resultSet.getColumnIndex('id'))constname=resultSet.getString(resultSet.getColumnIndex('name'))// ...}
  • goToNextRow():推进游标,返回false表示遍历完
  • getColumnIndex('列名'):拿列索引(数字)
  • getLong/getDouble/getString(列索引):按类型取值

ResultSet用完必须close()

resultSet.close()

ResultSet持有底层 SQLite 游标资源,不 close 会内存泄漏。这是新手最容易忘的坑

2.5beginTransaction/commit/rollBack:事务保证原子性

asyncrunTransaction():Promise<void>{if(!this.rdbStore){this.stateLog='尚未初始化 RDB,无法跑事务'return}try{// beginTransaction 开启事务this.rdbStore.beginTransaction()try{constnow:number=Date.now()// 批量插入 3 行for(leti=0;i<3;i++){constbucket:relationalStore.ValuesBucket={'name':`tx_user_${now}_${i}`,'age':20+i,'ts':now}awaitthis.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 全跑通的真机证明。


四、relationalStorevs 前端「IndexedDB + SQL.js」:啥差异

新手最容易纠结的问题:既然前端IndexedDB那么标准,鸿蒙为啥要造关系数据库?

维度前端「IndexedDB + SQL.js」relationalStore
运行环境浏览器宿主鸿蒙原生运行环境
底层引擎IndexedDB / SQLite-WASMSQLite 原生编译
API 风格异步回调 + 对象存储Promise + SQL 风格
安全模型同源策略鸿蒙沙箱 + SecurityLevel
事务 APItransaction隐式beginTransaction/commit显式
性能JS-WASM 桥接,慢原生 SQLite,快

一句话决策:鸿蒙应用存结构化数据必须用relationalStore,不能用IndexedDB(不存在)/localStorage(容量小 + 不能条件查)。鸿蒙不是浏览器,这套原生 SQLite 封装更安全可控、性能更高。


五、常见坑(都是血泪)

症状解法
localStorage/IndexedDB编译报错「找不到」鸿蒙用relationalStore,没浏览器宿主 API
executeSql拼 SQL 字符串SQL 注入风险查询用querySql,插入用insert + ValuesBucket
ResultSetclose()游标泄漏 + 后续查询卡用完务必resultSet.close()
事务忘rollBack异常时数据不一致try/catch 内 catch 调rollBack
跨应用共享数据库拿不到对方 RdbStore鸿蒙沙箱隔离,跨应用要 ohos.permission
SecurityLevel 选 S4普通应用拿不到 S4S4 限严格机密应用,普通用 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 系列第五篇,后续按这个顺序往下:

  1. WebSocket@ohos.net.webSocket(下一篇):长连接、推送、实时通讯,聊天应用必学
  2. 媒体访问@ohos.file.photoAccessHelper:访问相册、扫描媒体文件,应用调系统相册必学
  3. 推送通知@ohos.notificationManager:通知栏展示、点击拉起,离线触达必学
  4. 动画@ohos.arkui.animation:属性动画、转场动画,UI 进阶必学
  5. 相机@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,随便用,别告我