鸿蒙开发实战:SQLite 数据库的本地存储与高效管理
1. 鸿蒙开发,为什么我第一个想到的就是SQLite?
如果你刚开始接触鸿蒙应用开发,可能会被各种新概念和API搞得有点懵。但别担心,无论你是想做个记账本、一个简单的待办事项清单,还是一个需要离线缓存新闻资讯的阅读器,有一个技术点你几乎绕不开,那就是本地数据存储。想象一下,你开发的App,用户每次打开,之前记录的数据都没了,这体验得多糟糕?所以,学会把数据“记住”,是让应用变得可用的第一步。
在众多本地存储方案里,SQLite绝对是那个“老朋友”和“多面手”。我做了这么多年移动端开发,从早期的Android到现在的鸿蒙,SQLite几乎是我在考虑结构化数据本地存储时的首选。它不是什么高深莫测的黑科技,而是一个极其可靠、轻量且强大的工具。简单来说,它就像一个内置在你应用里的微型Excel,但功能更强大、管理更规范。你不用架设服务器,不用配置复杂的数据库环境,一个文件就搞定所有数据。
在鸿蒙(HarmonyOS)里,官方对SQLite的支持非常友好,提供了一套清晰明了的API。这意味着,你之前在其他平台(比如Android或iOS)上用SQLite的经验,绝大部分可以无缝迁移过来。这对于我们开发者来说,学习成本大大降低,可以更专注于业务逻辑的实现。接下来,我就带你从零开始,手把手把SQLite在鸿蒙里用起来,我会分享一些我实际项目中总结的窍门和踩过的坑,保证你听完就能上手实操。
2. 开工前准备:认识鸿蒙的“数据管家”
在写第一行代码之前,我们得先搞清楚鸿蒙给我们提供了什么工具。鸿蒙应用开发主要使用ArkTS语言(基于TypeScript),对于数据库操作,核心就是 @ohos.data.relationalStore 这个包。没错,你可能在网上搜到一些更早的资料提到 @ohos.data.sqlite,但在较新的API版本中,官方推荐使用功能更丰富、封装更好的 relationalStore 来操作关系型数据库,其底层依然是SQLite。
你可以把它理解为一个功能更强的“数据管家”。这个管家不仅会执行你的SQL命令(基础的增删改查),还额外提供了很多便利服务,比如:
- 对象关系映射(ORM)雏形:你可以用更接近写代码的方式(定义TypeScript类或对象)来操作数据,而不仅仅是拼接SQL字符串。
- 数据变更通知:当数据库里的数据发生变化时,可以自动通知到你的UI界面,这对于实现数据驱动UI更新(比如列表实时刷新)特别有用。
- 更便捷的查询:提供了链式调用的查询构建器,写起来更流畅,也更不容易出错。
当然,它100%兼容标准的SQL语法,所以你熟悉的那些SELECT * FROM table WHERE ...完全有效。我个人建议,尤其是初学者,先从理解标准SQL和基础API开始,然后再去探索relationalStore提供的高级特性,这样根基更牢。本文我们会以 @ohos.data.relationalStore 为主要工具进行讲解,因为这是当前和未来的方向。
首先,你需要在项目的 entry/src/main/ets/entryability/EntryAbility.ts 文件(或你的Ability文件)中,或者在具体页面的代码文件里,导入这个模块:
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';
3. 第一步:创建数据库与定义数据表
万事开头难,但数据库的“开头”其实挺简单。创建数据库和定义表结构,就像是给你要存储的数据盖个房子,并规划好每个房间(字段)的用途和规格。
3.1 初始化数据库环境
在鸿蒙里,数据库是和具体的应用上下文(Context)关联的。我们首先需要获取到上下文,然后配置数据库的基本信息。
// 假设在某个Page页面的aboutToAppear生命周期函数中,或在Ability的onCreate中
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
@Entry
@Component
struct DatabasePage {
// 声明一个关系型数据库对象
private rdbStore: relationalStore.RdbStore | undefined = undefined;
// 获取UIAbility的上下文,这里以页面获取为例
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
aboutToAppear() {
this.initRdbStore();
}
async initRdbStore() {
// 1. 数据库配置
const config: relationalStore.StoreConfig = {
name: 'myApp.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1 // 安全级别,S1表示数据库文件不加密
};
try {
// 2. 获取(或创建)RdbStore实例
this.rdbStore = await relationalStore.getRdbStore(this.context, config);
console.info('数据库初始化成功');
// 初始化成功后,紧接着创建表
await this.createTable();
} catch (err) {
const error = err as BusinessError;
console.error(`数据库初始化失败,错误码:${error.code}, 错误信息:${error.message}`);
}
}
}
这段代码做了两件关键事:一是定义了一个叫myApp.db的数据库文件;二是通过getRdbStore方法拿到了操作这个数据库的“手柄”(rdbStore)。如果这个数据库文件不存在,系统会自动创建它。
3.2 设计并创建你的第一张表
有了数据库,我们得在里面建表。表结构的设计是重中之重,好的设计能让后续的查询和维护事半功倍。我们以一个经典的“用户表”和“文章表”为例。
async createTable() {
if (!this.rdbStore) {
console.error('RdbStore未初始化,无法创建表');
return;
}
// 创建用户表的SQL语句
const userTableSql = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, -- 用户名,唯一且不能为空
email TEXT,
age INTEGER,
created_time TIMESTAMP DEFAULT (datetime('now','localtime')) -- 记录创建时间,默认当前本地时间
)
`;
// 创建文章表的SQL语句,并关联用户表
const articleTableSql = `
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
author_id INTEGER NOT NULL,
publish_time TIMESTAMP DEFAULT (datetime('now','localtime')),
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE -- 外键约束,用户删除时其文章也删除
)
`;
try {
await this.rdbStore.executeSql(userTableSql);
await this.rdbStore.executeSql(articleTableSql);
console.info('数据表创建成功');
} catch (err) {
const error = err as BusinessError;
console.error(`创建表失败,错误码:${error.code}, 错误信息:${error.message}`);
}
}
这里有几个设计要点值得一说:
- 主键:
id字段使用INTEGER PRIMARY KEY AUTOINCREMENT,这是SQLite的自增主键标准写法,每插入一条新记录,id会自动+1。 - 约束:
NOT NULL、UNIQUE这些约束能保证数据的完整性和一致性。比如用户名不能重复。 - 默认值:
DEFAULT (datetime('now','localtime'))这个技巧很实用,可以让数据库自动记录数据的创建时间,省去了在代码里手动设置。 - 表关系:在
articles表中,我们通过author_id字段和users表的id字段建立了关联,并用FOREIGN KEY定义了外键约束。ON DELETE CASCADE意味着当某个用户被删除时,他写的所有文章也会被自动删除,这保证了数据的一致性。注意:在鸿蒙的relationalStore中,外键约束默认可能是关闭的,需要在数据库配置中开启,这里为了示例清晰先这样写。
4. 核心玩法:增删改查(CRUD)实战
房子盖好了,接下来就是往里面搬家具、调整布局、清理旧物了。这就是我们常说的CRUD:Create(增)、Read(查)、Update(改)、Delete(删)。
4.1 增(Insert):把数据存进去
插入数据最直接的方式还是用SQL语句。但relationalStore也提供了更安全的参数化查询方式,可以有效防止SQL注入。
// 示例:插入一个新用户
async insertUser(username: string, email: string, age: number) {
if (!this.rdbStore) {
return;
}
const sql = 'INSERT INTO users (username, email, age) VALUES (?, ?, ?)';
const args: Array<ValueType> = [username, email, age]; // ValueType是 string|number|Uint8Array|null 的联合类型
try {
await this.rdbStore.executeSql(sql, args);
console.info(`用户 ${username} 插入成功`);
} catch (err) {
const error = err as BusinessError;
// 特别处理唯一约束冲突错误
if (error.code === 1) { // SQLite约束错误码,通常1代表SQLITE_ERROR,需根据具体信息判断
console.error(`用户名 ${username} 已存在,插入失败`);
} else {
console.error(`插入用户失败: ${error.message}`);
}
}
}
// 调用
this.insertUser('酷爱学习', 'coder@example.com', 28);
使用 ? 作为占位符,然后把实际参数放在args数组里传给executeSql,这是标准的安全做法。错误处理也很重要,比如捕获用户名重复的异常,给用户友好的提示。
4.2 查(Query):把数据读出来
查询是数据库操作中最频繁、也最能体现功力的部分。除了直接执行SELECT语句,relationalStore提供了强大的query方法,支持更灵活的查询构建。
方法一:直接执行SQL查询
async queryAllUsers() {
if (!this.rdbStore) {
return [];
}
const sql = 'SELECT * FROM users ORDER BY created_time DESC'; // 按创建时间倒序排列
try {
const resultSet: relationalStore.ResultSet = await this.rdbStore.querySql(sql);
console.info(`查询到 ${resultSet.rowCount} 条用户记录`);
// 遍历结果集
let users: Array<Record<string, ValueType>> = [];
while (resultSet.goToNextRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
const username = resultSet.getString(resultSet.getColumnIndex('username'));
const email = resultSet.getString(resultSet.getColumnIndex('email'));
users.push({ id, username, email });
console.info(`ID: ${id}, 用户名: ${username}, 邮箱: ${email}`);
}
resultSet.close(); // 非常重要!使用完毕后必须关闭结果集
return users;
} catch (err) {
const error = err as BusinessError;
console.error(`查询用户失败: ${error.message}`);
return [];
}
}
注意resultSet的使用流程:获取 -> 遍历 (goToNextRow) -> 按列名取数据 -> 最后一定要close()!不关闭会导致内存泄漏。
方法二:使用RdbPredicates构建查询(推荐)
这是relationalStore的特色功能,用起来更像编程,更安全。
async queryUsersByAge(minAge: number) {
if (!this.rdbStore) {
return [];
}
try {
// 1. 构建查询谓词
let predicates = new relationalStore.RdbPredicates('users'); // 指定查询的表
predicates.equalTo('age', minAge) // age = minAge
.or() // 或
.greaterThan('age', minAge) // age > minAge
.orderByDesc('created_time'); // 按创建时间降序
// 2. 执行查询
const resultSet: relationalStore.ResultSet = await this.rdbStore.query(predicates, ['id', 'username', 'email', 'age']); // 第二个参数指定要查询的列,不传则查所有列
// 3. 处理结果(同上)
let userList = [];
while(resultSet.goToNextRow()) {
userList.push({
id: resultSet.getLong(0), // 通过索引获取,顺序对应查询的列
username: resultSet.getString(1),
email: resultSet.getString(2),
age: resultSet.getLong(3)
});
}
resultSet.close();
return userList;
} catch (err) {
// ... 错误处理
}
}
RdbPredicates支持equalTo、notEqualTo、greaterThan、like、in、orderBy、limit等丰富的操作,可以构建非常复杂的查询条件,而且完全避免了SQL字符串拼接的错误和风险。
4.3 改(Update)与删(Delete)
更新和删除操作同样支持SQL和RdbPredicates两种方式。
更新用户邮箱(使用SQL):
async updateUserEmail(username: string, newEmail: string) {
if (!this.rdbStore) {
return false;
}
const sql = 'UPDATE users SET email = ? WHERE username = ?';
try {
const changes = await this.rdbStore.executeSql(sql, [newEmail, username]);
console.info(`更新了 ${changes} 条记录`);
return changes > 0;
} catch (err) {
// ... 错误处理
return false;
}
}
executeSql方法在执行UPDATE或DELETE后会返回受影响的行数,这很有用,可以判断是否真的更新或删除了数据。
删除用户(使用RdbPredicates):
async deleteUser(username: string) {
if (!this.rdbStore) {
return false;
}
try {
let predicates = new relationalStore.RdbPredicates('users');
predicates.equalTo('username', username);
const changes = await this.rdbStore.delete(predicates);
console.info(`删除了 ${changes} 条用户记录`);
return changes > 0;
} catch (err) {
// ... 错误处理
return false;
}
}
5. 进阶技巧:让数据库操作更稳健高效
掌握了基本的CRUD,你的应用已经能“记住”东西了。但要做得更专业,下面这些我踩过坑才总结出的经验,你一定要看看。
5.1 事务处理:保证数据的一致性
事务是数据库的核心概念。想象一下转账操作:从A账户扣钱,向B账户加钱。这两个操作必须同时成功或同时失败,否则数据就乱套了。这就是事务的用武之地。
async transferPoints(fromUser: string, toUser: string, points: number) {
if (!this.rdbStore) {
return false;
}
// 开始事务
await this.rdbStore.beginTransaction();
try {
// 1. 检查转出用户余额是否充足(这里简化,假设有个points字段)
const checkSql = 'SELECT points FROM users WHERE username = ?';
let resultSet = await this.rdbStore.querySql(checkSql, [fromUser]);
if (!resultSet.goToNextRow()) {
throw new Error('转出用户不存在');
}
const currentPoints = resultSet.getLong(resultSet.getColumnIndex('points'));
resultSet.close();
if (currentPoints < points) {
throw new Error('余额不足');
}
// 2. 执行扣款
const deductSql = 'UPDATE users SET points = points - ? WHERE username = ?';
await this.rdbStore.executeSql(deductSql, [points, fromUser]);
// 3. 执行加款
const addSql = 'UPDATE users SET points = points + ? WHERE username = ?';
await this.rdbStore.executeSql(addSql, [points, toUser]);
// 所有操作成功,提交事务
await this.rdbStore.commit();
console.info('转账成功');
return true;
} catch (err) {
// 任何一步出错,回滚事务,所有修改撤销
await this.rdbStore.rollback();
console.error('转账失败,事务已回滚:', err.message);
return false;
}
}
关键点:beginTransaction()、commit()、rollback()。把一系列必须原子化执行的数据库操作放在事务里,是编写健壮业务逻辑的必备技能。
5.2 数据库升级:当你的数据结构需要改变
应用版本迭代,增加新功能,原来的表结构可能不够用了。比如,我们要给users表增加一个avatar(头像)字段。你不能直接删了旧表重建,因为那样用户的老数据就全丢了。正确的做法是进行数据库版本升级。
这通常在初始化数据库的StoreConfig里配置,并配合一个onUpgrade回调函数。由于篇幅限制,这里简述其核心思想:在getRdbStore时,指定一个比当前存储的版本号更高的version,系统会触发升级。你在onUpgrade回调里,通过判断旧版本号到新版本号的变化,执行相应的ALTER TABLE等SQL语句来修改表结构,并迁移必要的数据。
5.3 性能优化小贴士
- 索引:对于经常用于
WHERE条件、JOIN连接或ORDER BY排序的字段,创建索引能极大提升查询速度。例如:CREATE INDEX idx_users_username ON users(username);。但索引不是免费的,它会增加插入、更新、删除操作的开销,并占用额外空间,所以不要滥用。 - 批量操作:需要插入大量数据时,务必使用事务将多次
INSERT包裹起来。我实测过,批量插入1000条数据,用事务比不用事务快几十倍甚至上百倍。 - 关闭ResultSet:再次强调,
query或querySql返回的ResultSet对象,用完后一定要调用close()方法释放资源。 - 避免在主线程进行大量IO操作:复杂的查询或大数据量的操作,可以考虑放到Worker线程中执行,防止阻塞UI,导致界面卡顿。
6. 实战:构建一个简单的本地记事本
光说不练假把式。我们综合运用上面的知识,快速勾勒一个本地记事本App的核心数据层代码。这个记事本能创建、查看、编辑、删除笔记。
首先,定义notes表:
const createNoteTableSql = `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_time TIMESTAMP DEFAULT (datetime('now','localtime')),
updated_time TIMESTAMP DEFAULT (datetime('now','localtime'))
)
`;
// 在initRdbStore后执行
await this.rdbStore.executeSql(createNoteTableSql);
然后,实现对应的数据操作类:
class NoteDataSource {
private rdbStore: relationalStore.RdbStore;
constructor(store: relationalStore.RdbStore) {
this.rdbStore = store;
}
// 新增或更新笔记
async saveNote(note: { id?: number, title: string, content: string }): Promise<number> {
let noteId = note.id;
if (noteId && noteId > 0) {
// 更新
const sql = 'UPDATE notes SET title = ?, content = ?, updated_time = datetime(\"now\",\"localtime\") WHERE id = ?';
await this.rdbStore.executeSql(sql, [note.title, note.content, noteId]);
} else {
// 新增
const sql = 'INSERT INTO notes (title, content) VALUES (?, ?)';
const result = await this.rdbStore.executeSql(sql, [note.title, note.content]);
// 获取最后插入的id
const resultSet = await this.rdbStore.querySql('SELECT last_insert_rowid() as id');
if (resultSet.goToNextRow()) {
noteId = resultSet.getLong(resultSet.getColumnIndex('id'));
}
resultSet.close();
}
return noteId as number;
}
// 查询所有笔记(按更新时间倒序)
async queryAllNotes(): Promise<Array<any>> {
const sql = 'SELECT * FROM notes ORDER BY updated_time DESC';
const resultSet = await this.rdbStore.querySql(sql);
const notes = [];
while (resultSet.goToNextRow()) {
notes.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
createdTime: resultSet.getString(resultSet.getColumnIndex('created_time')),
updatedTime: resultSet.getString(resultSet.getColumnIndex('updated_time'))
});
}
resultSet.close();
return notes;
}
// 删除笔记
async deleteNoteById(id: number): Promise<boolean> {
const sql = 'DELETE FROM notes WHERE id = ?';
const changes = await this.rdbStore.executeSql(sql, [id]);
return changes > 0;
}
}
在这个实战例子里,我们实现了笔记的保存(包含新增和更新)、列表查询和删除。saveNote方法通过判断是否有id来决定是插入还是更新,这是一个常见的做法。更新时,我们同时更新了updated_time字段。查询时按更新时间倒序,让最新的笔记排在最前面。
把这些方法和你鸿蒙应用的UI界面(比如List组件展示笔记列表,TextInput和Button用于编辑和保存)绑定起来,一个具备本地数据持久化功能的简易记事本就有了雏形。
更多推荐
所有评论(0)