【鸿蒙心迹】关系型数据库实战:从建表到复杂查询的全链路拆解

摘要:本文系统拆解 HarmonyOS 关系型数据库 RelationalStore 的完整使用链路。首先从整体认知入手,对比 RelationalStore 与 Preferences、分布式数据的差异,梳理 RdbStore、表、索引、事务、谓词、结果集等核心概念及适用边界;接着深入原理层,讲解 ArkTS 与 SQLite 的数据类型映射、建表与索引设计、查询执行流程、谓词查询机制、事务原子性以及 SQLCipher 加密数据库与安全等级;最后以一个打卡记录应用为例,完整演示建表、插入、查询、更新删除、批量事务五步开发流程,并给出常见踩坑排错路径与最佳实践清单。适合有 ArkTS 基础、希望正确落地本地数据库存储的开发者阅读。

数据持久化是应用开发的基础能力。HarmonyOS提供了关系型数据库(RelationalStore)作为本地数据存储的核心方案。我第一次用RelationalStore时,以为就是SQLite的简单封装,深入才发现鸿蒙在SQLite之上构建了"沙箱隔离+加密存储+分布式同步"的增强体系。最让人头疼的是数据类型映射——ArkTS的number在数据库里存成什么类型、bigint和int怎么选、加密数据库的密钥管理怎么做,每个细节都有坑。

如果你有ArkTS开发基础,想让你的应用正确使用本地数据库存储数据,但对RelationalStore不熟悉,这篇正好适合你。读完后你会知道:RelationalStore的架构和核心概念、建表与查询的完整流程、加密数据库与分布式同步机制。

本文分三层来讲:第一层建立对RelationalStore的整体认知;第二层拆解数据类型映射和查询优化原理;第三层是完整的实战落地和踩坑排错。

入门认知:RelationalStore到底"存"了什么

一句话定义

RelationalStore是鸿蒙基于SQLite提供的关系型数据库管理能力——支持建表、索引、事务、加密存储和分布式同步,应用通过RdbStore接口操作数据库,数据存储在应用沙箱内,保证数据隔离和安全。

打个比方:Preferences就像便签纸——简单但容量有限;RelationalStore就像档案柜——结构化存储、支持复杂查询、容量大、还能上锁(加密)。

RelationalStore vs Preferences vs 分布式数据

对比维度PreferencesRelationalStore分布式数据
数据结构键值对关系型(表/行/列)键值对/结构化
查询能力按key查找SQL查询按key查找
存储容量小(KB级)大(GB级)中
事务支持不支持支持支持
加密支持不支持支持支持
分布式同步不支持支持支持
适用场景配置/偏好业务数据跨端数据

表1:三种本地存储方案对比

全景图示

RelationalStore

核心接口 RdbStore

数据安全

分布式同步

建表/索引

增删改查

事务管理

沙箱隔离

加密存储

数据同步

冲突解决

图2:RelationalStore架构

核心概念

概念说明对应SQL
RdbStore数据库操作接口对象Connection
表(Table)数据存储结构CREATE TABLE
索引(Index)加速查询的结构CREATE INDEX
事务(Transaction)保证操作的原子性BEGIN/COMMIT
谓词(Predicate)查询条件构造器WHERE条件
结果集(ResultSet)查询返回的数据集SELECT结果

表2:RelationalStore核心概念

核心概念之间的协作关系可以这样理解:

RdbStore 数据库对象

表 Table

索引 Index

谓词 Predicate

查询 Query

结果集 ResultSet

事务 Transaction

图6:核心概念协作关系

适用边界

RelationalStore适合:需要存储大量结构化数据的应用、需要复杂查询(多表关联、聚合统计)的应用、需要数据加密的应用。不适合:简单的配置存储(用Preferences)、跨设备实时同步(用分布式数据对象)。

下面用一张决策图帮你快速判断该选哪种存储方案:

是

是

否

否

是

否

是

否

需要持久化数据

数据结构是键值对?

数据量小且无查询需求?

Preferences

RelationalStore

需要跨设备实时同步?

分布式数据对象

需要复杂查询或加密?

图1:本地存储方案选型决策

进阶原理:数据类型映射与查询优化

ArkTS与SQLite数据类型映射

ArkTS的类型系统和SQLite的类型系统不完全对应,RelationalStore做了映射:

ArkTS类型SQLite存储类型说明
numberREAL浮点数存储,整数也用REAL
stringTEXT文本类型
booleanINTEGER0=false, 1=true
ArrayBufferBLOB二进制数据
bigintINTEGER64位整数

表3:ArkTS与SQLite数据类型映射

建表与数据类型选择

// 建表SQL — 注意数据类型选择
const CREATE_TABLE_SQL = `
  CREATE TABLE IF NOT EXISTS user_records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_name TEXT NOT NULL,
    age INTEGER,
    score REAL,
    avatar BLOB,
    is_active INTEGER DEFAULT 1,
    created_at INTEGER,
    updated_at INTEGER
  )
`;

// 创建索引加速查询
const CREATE_INDEX_SQL = `
  CREATE INDEX IF NOT EXISTS idx_user_name ON user_records(user_name)
`;

查询执行流程

SQLiteRdbStoreAppSQLiteRdbStoreApp构建Predicate(查询条件)生成SQL语句执行查询(使用索引)返回结果集返回ResultSet遍历ResultSet读取数据

图3:查询执行流程

谓词(Predicate)查询机制

谓词是RelationalStore的核心查询构造器,链式调用构建WHERE条件:

// 谓词构建查询条件
const predicate = new relationalStore.RdbPredicates('user_records');
predicate.equalTo('is_active', 1)
         .greaterThan('age', 18)
         .orderByAsc('score')
         .limitAs(10);

// 等价SQL: SELECT * FROM user_records 
//          WHERE is_active = 1 AND age > 18 
//          ORDER BY score ASC LIMIT 10

事务机制

事务保证一组操作的原子性——要么全部成功,要么全部回滚:

// 事务保证原子性
try {
  rdbStore.beginTransaction();
  rdbStore.insert('user_records', { user_name: 'Alice', age: 25 });
  rdbStore.insert('user_records', { user_name: 'Bob', age: 30 });
  rdbStore.commit();
} catch (err) {
  rdbStore.rollBack();
  console.error('事务回滚');
}

事务的完整生命周期如下:

beginTransaction

全部成功 commit

出现异常 rollBack

开始事务

执行操作

提交

回滚

图7:事务生命周期

加密数据库

API 26支持数据库加密,使用SQLCipher提供透明加密:

// 创建加密数据库
const config: relationalStore.StoreConfig = {
  name: 'encrypted_db',
  securityLevel: relationalStore.SecurityLevel.S3,  // 安全等级
  encrypt: true  // 启用加密
};

const rdbStore = await relationalStore.getRdbStore(context, config);
安全等级说明适用场景
S1最低安全等级公开数据
S2低安全等级一般业务数据
S3中安全等级用户敏感数据
S4最高安全等级极敏感数据(密码、金融)

表4:数据库安全等级

加密数据库的创建与访问流程如下:

否

是

否

是

构造 StoreConfig

encrypt 是否为 true?

普通数据库

securityLevel 是否 S3 及以上?

创建失败

SQLCipher 透明加密

getRdbStore 打开加密库

正常增删改查

图4:加密数据库创建流程

实战落地:完整的数据库开发流程

场景:一个打卡记录应用的数据库设计

第一步:创建数据库和表
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';

const DB_NAME = 'checkin_db';
const STORE_CONFIG: relationalStore.StoreConfig = {
  name: DB_NAME,
  securityLevel: relationalStore.SecurityLevel.S2
};

let rdbStore: relationalStore.RdbStore | null = null;

async function initDatabase(context: common.UIAbilityContext): Promise<void> {
  rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
  
  // 创建打卡记录表
  const CREATE_CHECKIN_TABLE = `
    CREATE TABLE IF NOT EXISTS checkin_records (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id TEXT NOT NULL,
      checkin_time INTEGER NOT NULL,
      latitude REAL,
      longitude REAL,
      address TEXT,
      photo_path TEXT,
      remark TEXT,
      is_synced INTEGER DEFAULT 0
    )
  `;
  await rdbStore.executeSql(CREATE_CHECKIN_TABLE);

  // 创建索引
  const CREATE_INDEX_TIME = `
    CREATE INDEX IF NOT EXISTS idx_checkin_time ON checkin_records(checkin_time)
  `;
  const CREATE_INDEX_USER = `
    CREATE INDEX IF NOT EXISTS idx_user_id ON checkin_records(user_id)
  `;
  await rdbStore.executeSql(CREATE_INDEX_TIME);
  await rdbStore.executeSql(CREATE_INDEX_USER);
}
第二步:插入数据
interface CheckinRecord {
  user_id: string;
  checkin_time: number;
  latitude: number;
  longitude: number;
  address: string;
  photo_path: string;
  remark: string;
}

async function insertCheckinRecord(record: CheckinRecord): Promise<number> {
  if (!rdbStore) throw new Error('数据库未初始化');
  
  const valueBucket: relationalStore.ValuesBucket = {
    user_id: record.user_id,
    checkin_time: record.checkin_time,
    latitude: record.latitude,
    longitude: record.longitude,
    address: record.address,
    photo_path: record.photo_path,
    remark: record.remark,
    is_synced: 0
  };
  
  const rowId = await rdbStore.insert('checkin_records', valueBucket);
  console.info(`插入成功,rowId=${rowId}`);
  return rowId;
}
第三步:查询数据
async function queryTodayCheckins(userId: string): Promise<Array<CheckinRecord>> {
  if (!rdbStore) throw new Error('数据库未初始化');
  
  const todayStart = new Date();
  todayStart.setHours(0, 0, 0, 0);
  const todayEnd = new Date();
  todayEnd.setHours(23, 59, 59, 999);
  
  const predicate = new relationalStore.RdbPredicates('checkin_records');
  predicate.equalTo('user_id', userId)
           .between('checkin_time', todayStart.getTime(), todayEnd.getTime())
           .orderByDesc('checkin_time');
  
  const resultSet = await rdbStore.query(predicate, ['id', 'user_id', 'checkin_time', 'latitude', 'longitude', 'address', 'photo_path', 'remark']);
  
  const records: Array<CheckinRecord> = [];
  while (resultSet.goToNextRow()) {
    records.push({
      user_id: resultSet.getString(resultSet.getColumnIndex('user_id')),
      checkin_time: resultSet.getLong(resultSet.getColumnIndex('checkin_time')),
      latitude: resultSet.getDouble(resultSet.getColumnIndex('latitude')),
      longitude: resultSet.getDouble(resultSet.getColumnIndex('longitude')),
      address: resultSet.getString(resultSet.getColumnIndex('address')),
      photo_path: resultSet.getString(resultSet.getColumnIndex('photo_path')),
      remark: resultSet.getString(resultSet.getColumnIndex('remark'))
    });
  }
  resultSet.close();
  return records;
}
第四步:更新和删除数据
async function updateCheckinRemark(id: number, remark: string): Promise<void> {
  if (!rdbStore) throw new Error('数据库未初始化');
  
  const valueBucket: relationalStore.ValuesBucket = {
    remark: remark,
    is_synced: 0
  };
  
  const predicate = new relationalStore.RdbPredicates('checkin_records');
  predicate.equalTo('id', id);
  
  const rowsAffected = await rdbStore.update(valueBucket, predicate);
  console.info(`更新了${rowsAffected}行`);
}

async function deleteCheckinRecord(id: number): Promise<void> {
  if (!rdbStore) throw new Error('数据库未初始化');
  
  const predicate = new relationalStore.RdbPredicates('checkin_records');
  predicate.equalTo('id', id);
  
  const rowsAffected = await rdbStore.delete(predicate);
  console.info(`删除了${rowsAffected}行`);
}
第五步:批量操作(事务)
async function batchInsertRecords(records: Array<CheckinRecord>): Promise<void> {
  if (!rdbStore) throw new Error('数据库未初始化');
  
  try {
    rdbStore.beginTransaction();
    for (const record of records) {
      const valueBucket: relationalStore.ValuesBucket = {
        user_id: record.user_id,
        checkin_time: record.checkin_time,
        latitude: record.latitude,
        longitude: record.longitude,
        address: record.address,
        photo_path: record.photo_path,
        remark: record.remark,
        is_synced: 0
      };
      await rdbStore.insert('checkin_records', valueBucket);
    }
    rdbStore.commit();
    console.info('批量插入成功');
  } catch (err) {
    rdbStore.rollBack();
    console.error(`批量插入失败,已回滚: ${err.code} - ${err.message}`);
  }
}

整个打卡应用的数据库开发流程可以概括为下面这条链路:

是

否

第一步 创建数据库和表

第二步 插入数据

第三步 查询数据

第四步 更新和删除数据

第五步 批量操作(事务)

getRdbStore 初始化

executeSql 建表

CREATE INDEX 建索引

beginTransaction

循环 insert

全部成功?

commit

rollBack

图5:打卡应用数据库开发全流程

踩坑排错

问题原因解决方案
数据库创建失败securityLevel设置不当根据数据敏感度选择S1-S4
查询结果集为空列名拼写错误或大小写不一致列名区分大小写,确保一致
ResultSet未关闭导致内存泄漏遍历完忘记close每次query后必须close ResultSet
事务中insert失败但未回滚异常未捕获try-catch包裹并在catch中rollBack
加密数据库打开失败encrypt配置与安全等级不匹配加密需要S3及以上安全等级
BLOB数据读写异常ArrayBuffer与BLOB转换问题使用正确的序列化方式
自增ID不连续删除记录后ID不会回填这是SQLite正常行为,不要依赖ID连续

表5:常见踩坑与解决方案

遇到问题时可以按下面的排查路径逐层定位:

否

是

是

否

是

否

是

否

数据库操作异常

数据库是否创建成功?

检查 securityLevel 与 encrypt 配置

查询结果是否为空?

核对列名大小写与谓词条件

是否出现内存泄漏或锁定?

确认 ResultSet 已 close

事务数据是否不一致?

try-catch 中补 rollBack

检查 BLOB 序列化与 ID 依赖

图8:常见问题排查路径

最佳实践清单

  1. 合理设计表结构 — 字段类型选择要匹配实际数据,不要全用TEXT
  2. 为高频查询字段创建索引 — 索引加速查询但增加写入开销,按需创建
  3. ResultSet必须close — 不close会导致游标泄漏,最终数据库锁定
  4. 批量操作用事务 — 事务不仅保证原子性,还能大幅提升批量写入性能
  5. 谓词查询替代原始SQL — 谓词API更安全,避免SQL注入风险
  6. 时间戳用INTEGER存储 — 不要用TEXT存储时间,INTEGER便于范围查询和排序
  7. 敏感数据用加密数据库 — SecurityLevel S3及以上启用加密,保护用户隐私
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐