鸿蒙ArkData关系型数据库 CRUD 实战:员工管理系统的建库到查询
鸿蒙ArkData关系型数据库 CRUD 实战:员工管理系统的建库到查询
关系型数据库(RelationalStore)是 ArkData 里能力最全的存储,基于 SQLite,支持事务、索引、谓词查询、自定义 SQL。下面用一个"员工管理系统"的完整案例,把建库建表、版本升级、增删改查、谓词查询、结果集遍历走一遍,重点讲清楚版本升级用事务保证原子性这个最佳实践。
一、案例背景:员工信息管理
一个公司员工管理系统,员工信息包括:姓名、年龄、薪资、编码(二进制)、地址。数据有明显的关系结构,适合关系型数据库。
表结构设计:
CREATE TABLE IF NOT EXISTS EMPLOYEE (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
AGE INTEGER,
SALARY REAL,
CODES BLOB,
ADDRESS TEXT
)
二、第一步:配置 StoreConfig
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
const DOMAIN = 0x0000;
let store: relationalStore.RdbStore | undefined = undefined;
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'RdbTest.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S3, // 安全级别
encrypt: false, // 是否加密
customDir: 'customDir/subCustomDir', // 自定义路径(可选)
isReadOnly: false, // 是否只读
};
几个配置要点:
name:数据库文件名,不同 context 下同名会产生不同数据库securityLevel:S1-S4,按数据敏感度选encrypt:敏感数据建议开isReadOnly: true时只允许读,写会返回错误码 801
三、第二步:建库建表 + 版本升级(事务保证原子性)
这是官方示例里最值得学习的部分——用事务保证数据库升级流程的原子性。升级过程中任何一步失败,整个升级回滚,不会留下半升级的烂摊子。
const SQL_CREATE_TABLE =
'CREATE TABLE IF NOT EXISTS EMPLOYEE (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT NOT NULL, AGE INTEGER, SALARY REAL, CODES BLOB, ADDRESS TEXT)';
async function initDatabase(context: common.UIAbilityContext) {
if (store === undefined) {
try {
store = await relationalStore.getRdbStore(context, STORE_CONFIG);
} catch (e) {
const err = e as BusinessError;
hilog.error(DOMAIN, 'rdb', `获取 RdbStore 失败: ${err.message}`);
return;
}
}
if (store !== undefined) {
// 创建事务对象
let transaction = await store.createTransaction({});
// 读取当前数据库版本
let storeVersion = await transaction.execute('PRAGMA user_version');
// 版本 0 → 1:初次建表
if (storeVersion === 0) {
try {
await transaction.execute(SQL_CREATE_TABLE);
storeVersion = 1;
hilog.info(DOMAIN, 'rdb', '升级 0 → 1 成功');
} catch (e) {
await transaction.rollback(); // 失败回滚
hilog.error(DOMAIN, 'rdb', `建表失败: ${(e as BusinessError).message}`);
return;
}
}
// 版本 1 → 2:新增 IDENTITY 列
if (storeVersion === 1) {
try {
await transaction.execute('ALTER TABLE EMPLOYEE ADD COLUMN IDENTITY UNLIMITED INT');
storeVersion = 2;
hilog.info(DOMAIN, 'rdb', '升级 1 → 2 成功');
} catch (e) {
await transaction.rollback();
hilog.error(DOMAIN, 'rdb', `升级失败: ${(e as BusinessError).message}`);
return;
}
}
// 版本 2 → 3:删除 ADDRESS 列
if (storeVersion === 2) {
try {
await transaction.execute('ALTER TABLE EMPLOYEE DROP COLUMN ADDRESS');
storeVersion = 3;
await transaction.execute('PRAGMA user_version = 3');
hilog.info(DOMAIN, 'rdb', '升级 2 → 3 成功');
} catch (e) {
await transaction.rollback();
hilog.error(DOMAIN, 'rdb', `升级失败: ${(e as BusinessError).message}`);
return;
}
}
await transaction.commit(); // 全部成功,提交事务
}
}
为什么用事务:升级逻辑较多时,如果不用事务,中途失败会导致数据库处于中间版本,后续启动无法正确判断状态。事务保证"要么全升完,要么完全不升",状态始终一致。
官方也提醒:升级逻辑多时建议拆成多个独立事务串行执行,本例因为流程短用了单个事务。
四、第三步:插入数据
async function insertEmployee() {
const valueBucket: relationalStore.ValuesBucket = {
NAME: 'Lisa',
AGE: 18,
SALARY: 100.5,
CODES: new Uint8Array([1, 2, 3, 4, 5]), // BLOB 类型用 Uint8Array
IDENTITY: BigInt('15822401018187971961171'), // bigint 用 UNLIMITED INT
};
if (store !== undefined) {
try {
const rowId = await store.insert('EMPLOYEE', valueBucket);
hilog.info(DOMAIN, 'rdb', `插入成功,rowId: ${rowId}`);
} catch (error) {
hilog.error(DOMAIN, 'rdb', `插入失败: ${(error as BusinessError).message}`);
}
}
}
要点:
- 关系型数据库没有显式 flush:插入即持久化,不需要像 Preferences 那样调 flush
- BLOB 用 Uint8Array:二进制数据(图片缩略图、加密密文等)用 Uint8Array
- bigint 用 UNLIMITED INT:超大整数在 SQL 里声明为 UNLIMITED INT
- 单条数据建议不超过 2MB:超过能插入但读取会失败
五、第四步:更新和删除(谓词驱动)
关系型数据库的更新和删除用**谓词(RdbPredicates)**指定条件,比拼 SQL 字符串更安全(防注入)。
// 更新:把 NAME 为 'Lisa' 的员工信息改掉
async function updateEmployee() {
const valueBucket: relationalStore.ValuesBucket = {
NAME: 'Rose',
AGE: 22,
SALARY: 200.5,
CODES: new Uint8Array([1, 2, 3, 4, 5]),
IDENTITY: BigInt('15822401018187971967863'),
};
let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
predicates.equalTo('NAME', 'Lisa'); // 条件:NAME = 'Lisa'
if (store !== undefined) {
const rows = await store.update(valueBucket, predicates);
hilog.info(DOMAIN, 'rdb', `更新行数: ${rows}`);
}
}
// 删除:删掉 NAME 为 'Lisa' 的员工
async function deleteEmployee() {
let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
predicates.equalTo('NAME', 'Lisa');
if (store !== undefined) {
const rows = await store.delete(predicates);
hilog.info(DOMAIN, 'rdb', `删除行数: ${rows}`);
}
}
谓词支持链式调用,能拼出复杂条件:
let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
predicates.greaterThan('AGE', 20)
.and()
.lessThan('SALARY', 200)
.orderByAsc('AGE');
六、第五步:查询与结果集遍历
async function queryEmployee() {
let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
predicates.equalTo('NAME', 'Rose');
if (store !== undefined) {
const resultSet = await store.query(predicates, ['ID', 'NAME', 'AGE', 'SALARY', 'CODES', 'IDENTITY']);
hilog.info(DOMAIN, 'rdb', `列名: ${resultSet.columnNames}, 列数: ${resultSet.columnCount}`);
// resultSet 是游标,默认指向第 -1 条,有效数据从 0 开始
while (resultSet.goToNextRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('ID'));
const name = resultSet.getString(resultSet.getColumnIndex('NAME'));
const age = resultSet.getLong(resultSet.getColumnIndex('AGE'));
const salary = resultSet.getDouble(resultSet.getColumnIndex('SALARY'));
const identity = resultSet.getValue(resultSet.getColumnIndex('IDENTITY'));
hilog.info(DOMAIN, 'rdb', `id=${id}, name=${name}, age=${age}, salary=${salary}`);
}
// 用完必须关闭,释放内存
resultSet.close();
}
}
结果集使用三步曲:
goToNextRow()移动游标,返回 false 表示遍历完getColumnIndex('字段名')拿列索引,再getLong/getString/getDouble取值close()释放内存——这步最容易被忘,长期不 close 会内存泄漏
七、第六步:自定义 SQL 查询
复杂查询用谓词写不出来时,直接跑 SQL:
async function customQuery() {
if (store !== undefined) {
// querySql 用于查询,返回结果集
const resultSet = await store.querySql(
'SELECT NAME, AVG(SALARY) as avg_salary FROM EMPLOYEE GROUP BY NAME HAVING AVG(SALARY) > ?',
[100]
);
while (resultSet.goToNextRow()) {
const name = resultSet.getString(resultSet.getColumnIndex('NAME'));
const avgSalary = resultSet.getDouble(resultSet.getColumnIndex('avg_salary'));
hilog.info(DOMAIN, 'rdb', `${name} 平均薪资: ${avgSalary}`);
}
resultSet.close();
}
}
querySql 第二个参数是参数化绑定(? 占位),能防 SQL 注入。永远不要用字符串拼接构造 SQL,那是安全漏洞的温床。
八、第七步:删除数据库
async function deleteDatabase(context: common.UIAbilityContext) {
relationalStore.deleteRdbStore(context, 'RdbTest.db', (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN, 'rdb', `删除失败: ${err.message}`);
return;
}
hilog.info(DOMAIN, 'rdb', '数据库删除成功');
});
}
九、约束限制速查
| 约束 | 说明 |
|---|---|
| 默认日志模式 | WAL(Write Ahead Log) |
| 默认落盘方式 | FULL 模式 |
| 连接数 | 常驻 4 读 + 1 写,读可动态扩充,写不可 |
| 并发写 | 同一时间只支持一个写操作 |
| 单条数据大小 | 建议 ≤ 2MB,超过能插但读失败 |
| 卸载 | 数据库文件及临时文件自动清除 |
| 支持类型 | number、string、二进制、boolean |
十、几个容易出问题的地方
10.1 -wal 和 -shm 临时文件
使用数据库时,同目录下会产生 -wal 和 -shm 结尾的临时文件。如果要移动数据库文件到别处查看,必须连同这两个临时文件一起移动,否则数据可能不完整。
10.2 大数据量查询卡顿
官方明确建议:
- 单次查询数据量不超过 5000 条
- 在 TaskPool 中查询(避免阻塞主线程)
- SQL 拼接尽量简洁
- 合理分批次查询
10.3 数据库异常重建
操作过程中可能抛出错误码 14800011,这是非预期的数据库异常。此时需要重建数据库并恢复数据,具体见下一篇的备份恢复实战。
10.4 context 要对
getRdbStore 和 context 强相关。即使用同名数据库,不同 context(比如不同 UIAbility)会产生不同数据库。多 UIAbility 共享数据时要注意 context 的选择。
十一、总结一下下
- 版本升级用事务:保证原子性,失败回滚,状态一致
- 插入即持久化:不需要 flush
- 更新删除用谓词:防注入,链式条件更清晰
- 结果集必须 close:否则内存泄漏
- SQL 用参数化绑定:永远不要字符串拼接
- 大数据量分批查:单次 ≤5000 条,放 TaskPool 里跑
更多推荐


所有评论(0)