RDB关系数据库实战:增删改查、事务、分页、版本迁移
RDB关系数据库实战:增删改查、事务、分页、版本迁移
本地数据库是鸿蒙应用里绕不开的能力。聊天记录、离线缓存、表单草稿、用户收藏,这些数据量大、结构复杂、需要条件查询的场景,用Preferences键值存储根本扛不住,必须上关系型数据库。很多开发者第一次接RDB时,总觉得SQLite相关的API繁琐:建表、增删改查、分页、事务、版本升级,一套下来步骤多,稍不注意就表结构出错。本文基于最新鸿蒙API,把RDB的完整落地流程拆成几个环节讲清楚,最后给一个可直接复用的工具类,代码可以直接复制到项目里跑。
01 数据库初始化与建表
RDB(Relational Database)是鸿蒙提供的关系型数据库,底层基于SQLite,支持标准SQL语句。使用前需要先配置数据库参数,创建数据库实例,然后建表。
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'my_database.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1,
encrypt: false
};
let rdb: relationalStore.RelationalStore | null = null;
// 初始化数据库
async function initDatabase(context: common.Context) {
rdb = await relationalStore.getRdbStore(context, STORE_CONFIG);
// 建表SQL
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT UNIQUE,
created_at INTEGER,
status INTEGER DEFAULT 1
)
`;
await rdb.executeSql(CREATE_TABLE_SQL);
console.info('数据库初始化完成');
}
几个关键点:
-
securityLevel:数据库安全级别,S1是基础级别,普通业务用S1就够了
-
CREATE TABLE IF NOT EXISTS:加上IF NOT EXISTS,重复执行不会报错,首次启动和版本升级都能安全执行
-
主键自增:INTEGER PRIMARY KEY AUTOINCREMENT,id自动增长,不用手动赋值
建表SQL写在初始化里,每次启动都会执行一次。表已存在的话直接跳过,不会重建。
02 增删改查基础操作
RDB提供了insert、update、delete、query四个基础方法,对应SQL的增删改查。不用自己拼接完整SQL,用参数化的方式调用更安全。
async function insertUser(name: string, age: number, email: string) {
if (!rdb) return;
// 用ValuesBucket存数据,key是列名
const valueBucket: relationalStore.ValuesBucket = {
name: name,
age: age,
email: email,
created_at: Date.now(),
status: 1
};
// 插入,返回新行的id
const rowId = await rdb.insert('user', valueBucket);
console.info('插入成功,id:', rowId);
return rowId;
}
async function updateUserStatus(userId: number, status: number) {
if (!rdb) return;
const valueBucket: relationalStore.ValuesBucket = {
status: status
};
// 更新条件:id = ?
const predicates = new relationalStore.RdbPredicates('user');
predicates.equalTo('id', userId);
// 返回受影响的行数
const rows = await rdb.update(valueBucket, predicates);
console.info('更新行数:', rows);
}
async function deleteUser(userId: number) {
if (!rdb) return;
const predicates = new relationalStore.RdbPredicates('user');
predicates.equalTo('id', userId);
const rows = await rdb.delete(predicates);
console.info('删除行数:', rows);
}
async function queryUserById(userId: number) {
if (!rdb) return;
const predicates = new relationalStore.RdbPredicates('user');
predicates.equalTo('id', userId);
// 查询列,null表示查所有列
const resultSet = await rdb.query(predicates, null);
// 遍历结果集
if (resultSet.goToFirstRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
const name = resultSet.getString(resultSet.getColumnIndex('name'));
const age = resultSet.getLong(resultSet.getColumnIndex('age'));
console.info('查询到用户:', id, name, age);
}
// 用完必须关闭结果集,不然内存泄漏
resultSet.close();
}
RdbPredicates是条件构造器,equalTo、greaterThan、like、orderByAsc这些方法都有,不用自己拼WHERE语句。
结果集ResultSet用完一定要close(),这个最容易忘。不close的话,数据库游标一直占着内存,查多了就会泄漏。
03 分页查询实现
列表页数据多了不能一次全查出来,必须分页。RDB的分页用limit和offset实现,对应SQL的LIMIT offset, count。
async function queryUserPage(page: number, pageSize: number) {
if (!rdb) return;
// 页码从1开始,计算偏移量
const offset = (page - 1) * pageSize;
const predicates = new relationalStore.RdbPredicates('user');
// 按创建时间倒序,最新的在前面
predicates.orderByDesc('created_at');
// 分页:offset是起始位置,pageSize是每页数量
predicates.limitAs(pageSize, offset);
const resultSet = await rdb.query(predicates, null);
const list: Array<{id: number, name: string, age: number}> = [];
while (resultSet.goToNextRow()) {
list.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
age: resultSet.getLong(resultSet.getColumnIndex('age'))
});
}
resultSet.close();
console.info('第', page, '页数据:', list);
return list;
}
分页查询的几个注意点:
-
页码从1开始,offset = (page - 1) * pageSize,别算错
-
必须有排序,不然分页数据可能重复或者漏行
-
总条数单独查,分页的同时查一下总数,前端才能显示共多少条
async function getUserCount(): Promise<number> {
if (!rdb) return 0;
const predicates = new relationalStore.RdbPredicates('user');
const resultSet = await rdb.querySql('SELECT COUNT(*) as count FROM user');
let count = 0;
if (resultSet.goToFirstRow()) {
count = resultSet.getLong(resultSet.getColumnIndex('count'));
}
resultSet.close();
return count;
}
04 事务处理
批量操作的时候,一条一条插入性能很差,而且中间出错了,前面的数据已经写进去了,回滚不了。这时候就要用事务——要么全部成功,要么全部失败。
async function batchInsertUsers(users: Array<{name: string, age: number}>) {
if (!rdb) return;
// 开启事务
rdb.begin();
try {
for (const user of users) {
const valueBucket: relationalStore.ValuesBucket = {
name: user.name,
age: user.age,
created_at: Date.now(),
status: 1
};
await rdb.insert('user', valueBucket);
}
// 全部成功,提交事务
rdb.commit();
console.info('批量插入成功');
} catch (e) {
// 出错了,回滚事务,数据全部撤销
rdb.rollBack();
console.error('批量插入失败,已回滚:', e);
}
}
事务的核心就是begin → commit / rollBack三个方法。批量插入、批量更新,一定要包在事务里,性能提升几十倍,而且数据安全。
有个细节:commit之后事务就结束了,不能再用同一个事务对象继续操作。要开新事务得重新begin。
05 数据库版本迁移
应用升级的时候,表结构经常要改:加字段、改字段类型、加索引。这时候不能直接删库重建,不然用户数据全丢了。RDB提供了版本升级的回调机制。
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'my_database.db',
securityLevel: relationalStore.SecurityLevel.S1,
encrypt: false,
// 数据库版本号,从1开始,每次升级加1
version: 2
};
async function initDatabaseWithMigration(context: common.Context) {
rdb = await relationalStore.getRdbStore(context, STORE_CONFIG);
// 版本升级回调
rdb.on('databaseUpgrade', (oldVersion: number, newVersion: number) => {
console.info('数据库升级:', oldVersion, '→', newVersion);
// 从版本1升级到版本2:加一个phone字段
if (oldVersion === 1 && newVersion === 2) {
rdb?.executeSql('ALTER TABLE user ADD COLUMN phone TEXT');
}
// 从版本2升级到版本3:加索引
if (oldVersion === 2 && newVersion === 3) {
rdb?.executeSql('CREATE INDEX IF NOT EXISTS idx_user_email ON user(email)');
}
});
}
版本迁移的规则:
-
version每次只加1,不要跳版本号,不然升级逻辑要处理跨版本的情况
-
ALTER TABLE加字段是最安全的,不影响老数据
-
删字段、改字段类型比较麻烦,得建新表→搬数据→删旧表,慎用
-
迁移SQL要加IF NOT EXISTS,防止重复执行报错
版本号写在StoreConfig里,系统自动对比当前数据库的版本,不一致就触发upgrade回调。不用自己判断版本号。
06 完整工具类封装
把上面的逻辑封装成一个通用的数据库工具类,项目里直接调用,不用每个页面都写一遍初始化和查询代码。
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
class DBHelper {
private static instance: DBHelper;
private rdb: relationalStore.RelationalStore | null = null;
private constructor() {}
static getInstance(): DBHelper {
if (!DBHelper.instance) {
DBHelper.instance = new DBHelper();
}
return DBHelper.instance;
}
// 初始化,在Ability启动时调用一次
async init(context: common.Context) {
const config: relationalStore.StoreConfig = {
name: 'my_database.db',
securityLevel: relationalStore.SecurityLevel.S1,
version: 1
};
this.rdb = await relationalStore.getRdbStore(context, config);
// 建表
await this.rdb.executeSql(`
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT UNIQUE,
created_at INTEGER
)
`);
}
// 插入
async insert(table: string, values: relationalStore.ValuesBucket): Promise<number> {
if (!this.rdb) return -1;
return await this.rdb.insert(table, values);
}
// 查询
async query(table: string, predicates: relationalStore.RdbPredicates) {
if (!this.rdb) return null;
return await this.rdb.query(predicates, null);
}
// 更新
async update(table: string, values: relationalStore.ValuesBucket, where: string) {
if (!this.rdb) return 0;
const predicates = new relationalStore.RdbPredicates(table);
// 这里简化,实际项目里可以扩展更多条件
return await this.rdb.update(values, predicates);
}
// 删除
async delete(table: string, predicates: relationalStore.RdbPredicates) {
if (!this.rdb) return 0;
return await this.rdb.delete(predicates);
}
// 执行原生SQL
async execSql(sql: string) {
if (!this.rdb) return;
await this.rdb.executeSql(sql);
}
}
// 使用方式
const db = DBHelper.getInstance();
// 在EntryAbility的onCreate里调用db.init(context)
// 页面里直接 db.insert('user', {...})
这样封装之后,任何页面只需要拿DBHelper.getInstance()就能操作数据库,不用关心初始化和连接管理。
小结
RDB关系数据库的核心就是四件事:建表、增删改查、分页、事务。基础操作不复杂,容易踩坑的地方主要是结果集没close、分页没排序、批量操作没包事务、版本升级丢数据。实际项目中,把数据库操作封装在统一的工具类里,表结构和SQL集中管理。版本号每次升级只加1,迁移逻辑写清楚,老用户升级也不会丢数据。
更多推荐




所有评论(0)