鸿蒙 韶非 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'// ...}三个细节:
import relationalStore from '@ohos.data.relationalStore'——relationalStore是关系数据库的入口模块context: common.UIAbilityContext——RDB 是应用沙箱内建库,需 UIAbility 上下文定位沙箱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-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,随便用,别告我