写在前面

如果你写过鸿蒙 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'
  // ...
}

三个细节:

  1. import relationalStore from '@ohos.data.relationalStore'——relationalStore 是关系数据库的入口模块
  2. context: common.UIAbilityContext——RDB 是应用沙箱内建库,需 UIAbility 上下文定位沙箱
  3. 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
ResultSetclose() 游标泄漏 + 后续查询卡 用完务必 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 系列第五篇,后续按这个顺序往下:

  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,随便用,别告我

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐