鸿蒙 韶非 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事务管理「保证原子性,要么全成要么全回滚」记住这五个,往下看。二、动手:一个建库 建表 插入 查询 事务的 demo2.1 import 拿 UIAbilityContextimportrelationalStorefromohos.data.relationalStoreimportcommonfromohos.app.ability.commonEntryComponentstruct Index{privatecontext:common.UIAbilityContextgetContext(this)ascommon.UIAbilityContextprivaterdbStore:relationalStore.RdbStore|nullnullprivatedbName:stringarkts_demo.dbprivatetableName:stringUSER// ...}三个细节:import relationalStore from ohos.data.relationalStore——relationalStore是关系数据库的入口模块context: common.UIAbilityContext——RDB 是应用沙箱内建库,需 UIAbility 上下文定位沙箱rdbStore存拿到的 RdbStore 实例,后续所有操作都走它2.2getRdbStoreexecuteSql:初始化 建表asyncinitRdb():Promisevoid{this.stateLog初始化 RDB 中...try{constconfig:relationalStore.StoreConfig{name:this.dbName,securityLevel:relationalStore.SecurityLevel.S1}// getRdbStore 拿 RdbStore 实例沙箱内建库this.rdbStoreawaitrelationalStore.getRdbStore(this.context,config)// 建表 SQLCREATE TABLE IF NOT EXISTSconstcreateSqlCREATE TABLE IF NOT EXISTS${this.tableName}( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, ts INTEGER )awaitthis.rdbStore.executeSql(createSql)this.stateLogRDB 已初始化,数据库 ${this.dbName},表 ${this.tableName}}catch(e){this.stateLog初始化失败:${e.message}this.rdbStorenull}}getRdbStore三个关键点:①StoreConfig配置:数据库名 安全级别constconfig:relationalStore.StoreConfig{name:this.dbName,// 数据库文件名沙箱内securityLevel:relationalStore.SecurityLevel.S1// 安全级别}SecurityLevel是鸿蒙定义的数据库安全级别:SecurityLevel含义用途S1低级公开数据普通应用S2中级个人数据记账/笔记S3高级敏感数据隐私相册S4最高严格机密金融/医疗S1 是默认选择。如果存用户隐私数据,选 S2/S3。②executeSql执行 DDL/原始 SQLawaitthis.rdbStore.executeSql(CREATE TABLE IF NOT EXISTS USER (...))executeSql执行建表/索引/原始 SQL,返回Promisevoid。注意:executeSql不返回查询结果——查询要走querySql。2.3insertValuesBucket:安全插入asyncinsertData():Promisevoid{if(!this.rdbStore){this.stateLog尚未初始化 RDB,无法插入return}try{constnow:numberDate.now()// ValuesBucket 键值对插入,比 raw SQL 安全防注入constbucket:relationalStore.ValuesBucket{name:user_${this.insertCount1},age:18(this.insertCount%30),ts:now}// insert 返回 rowIdconstrowId:numberawaitthis.rdbStore.insert(this.tableName,bucket)this.insertCountthis.stateLog第${this.insertCount}次插入成功,rowId ${rowId}}catch(e){this.stateLog插入失败:${e.message}}}insert三个关键点:①ValuesBucket键值对插入防 SQL 注入constbucket:relationalStore.ValuesBucket{name:user_${this.insertCount1},// 列名 - 值age:18(this.insertCount%30),ts:now}awaitthis.rdbStore.insert(this.tableName,bucket)ValuesBucket是「列名 - 值」的键值对。鸿蒙内部用参数化查询拼接,自动防 SQL 注入。比手写INSERT INTO ... VALUES (...)安全得多——手写 raw SQL 拼字符串,用户输入含就炸。②insert返回rowIdconstrowId:numberawaitthis.rdbStore.insert(this.tableName,bucket)rowId是新插入行的主键自增 ID,后续更新/删除可用它索引。2.4querySqlResultSet:查询遍历asyncqueryData():Promisevoid{if(!this.rdbStore){this.stateLog尚未初始化 RDB,无法查询return}try{// querySql 执行 SELECT,返回 ResultSetconstresultSet:relationalStore.ResultSetawaitthis.rdbStore.querySql(SELECT id, name, age, ts FROM${this.tableName}ORDER BY id DESC LIMIT 5)constrows:string[][]// ResultSet 遍历: goToFirstRow / goToNextRow / getColumnIndex / getString/getLongfor(leti0;resultSet.goToNextRow();i){constid:numberresultSet.getLong(resultSet.getColumnIndex(id))constname:stringresultSet.getString(resultSet.getColumnIndex(name))constage:numberresultSet.getLong(resultSet.getColumnIndex(age))rows.push(id${id}, name${name}, age${age})}// resultSet 用完必须 close 释放resultSet.close()this.queryCountthis.lastQueryResultrows.length0?rows.join(\n):(表为空)this.stateLog第${this.queryCount}次查询成功,返回${rows.length}行}catch(e){this.stateLog查询失败:${e.message}}}querySqlResultSet三个关键点:①querySql执行 SELECT 返回ResultSetconstresultSetawaitthis.rdbStore.querySql(SELECT ... FROM ...)ResultSet是游标,不是数组——初始指向「第一行之前」,要主动goToNextRow推进。②ResultSet遍历:游标推进 列索引取值for(leti0;resultSet.goToNextRow();i){constidresultSet.getLong(resultSet.getColumnIndex(id))constnameresultSet.getString(resultSet.getColumnIndex(name))// ...}goToNextRow():推进游标,返回false表示遍历完getColumnIndex(列名):拿列索引数字getLong/getDouble/getString(列索引):按类型取值③ResultSet用完必须close()resultSet.close()ResultSet持有底层 SQLite 游标资源,不 close 会内存泄漏。这是新手最容易忘的坑。2.5beginTransaction/commit/rollBack:事务保证原子性asyncrunTransaction():Promisevoid{if(!this.rdbStore){this.stateLog尚未初始化 RDB,无法跑事务return}try{// beginTransaction 开启事务this.rdbStore.beginTransaction()try{constnow:numberDate.now()// 批量插入 3 行for(leti0;i3;i){constbucket:relationalStore.ValuesBucket{name:tx_user_${now}_${i},age:20i,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」——getRdbStoreexecuteSql真建库建表了;插入次数 2 最近查询结果显示 5 行——insertquerySql真增真查了;事务日志「事务提交成功,批量插入 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,没浏览器宿主 APIexecuteSql拼 SQL 字符串SQL 注入风险查询用querySql,插入用insert ValuesBucketResultSet忘close()游标泄漏 后续查询卡用完务必resultSet.close()事务忘rollBack异常时数据不一致try/catch 内 catch 调rollBack跨应用共享数据库拿不到对方 RdbStore鸿蒙沙箱隔离,跨应用要 ohos.permissionSecurityLevel 选 S4普通应用拿不到 S4S4 限严格机密应用,普通用 S1/S2querySql期望返回数组编译报错类型不匹配返回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主页面getRdbStoreexecuteSqlinsert/ValuesBucketquerySql/ResultSetbeginTransaction/commit/rollBack五姿势StoreConfig配置 SecurityLevel分级说明可直接用 DevEco Studio 打开运行真机装普通应用必能跑八、下一步该学什么?跑通这个 demo 之后,你的鸿蒙结构化存取就入门了。这是韶非 UI 系列第五篇,后续按这个顺序往下:WebSocketohos.net.webSocket下一篇:长连接、推送、实时通讯,聊天应用必学媒体访问ohos.file.photoAccessHelper:访问相册、扫描媒体文件,应用调系统相册必学推送通知ohos.notificationManager:通知栏展示、点击拉起,离线触达必学动画ohos.arkui.animation:属性动画、转场动画,UI 进阶必学相机ohos.multimedia.camera:预览、拍照、录像,相机应用必学写在最后relationalStore的本质,是**「鸿蒙给应用存结构化数据的原生 SQLite 封装」**——不是浏览器IndexedDB,是鸿蒙专门给关系型存取的原生机制,能力对标「IndexedDB SQL.js」但更安全可控、性能更高。代价是ValuesBucket键值对插入多一步、ResultSet游标遍历多一步。一旦你开始用关系数据库思维写结构化存取,你会发现大部分「记账应用按月查开销」「笔记应用按标签筛笔记」「待办按截止时间排」的需求,都是getRdbStoreexecuteSqlinsert/ValuesBucketquerySql/ResultSet的自然结果。代码量比localStorage多两行,结构化查询能力高九成。代码已经给你了,仓库链接在上面。现在关掉这篇文章,打开 DevEco Studio,把 demo 跑起来,亲手点建库建表插入查询事务五大姿势感受下结构化存取。跑通了,回来评论区打个「1」,我看看有多少人真的动手了。作者:JaneConan仓库:https://atomgit.com/JaneConan/arkui-rdb协议:Apache-2.0,随便用,别告我