08 关系型数据库入门------待办事项持久化
移动应用中的数据持久化是核心能力之一。HarmonyOS 提供了 @kit.ArkData 中的关系型数据库(RDB)API,支持完整的 SQL 操作。本文将以「柚兔学伴」项目的 TodoDatabase.ets 为例,从数据库初始化到增删改查,全面讲解关系型数据库的使用方法。
1. 数据库配置与表结构
1.1 StoreConfig 配置
typescript
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'TodoDatabase.db',
securityLevel: relationalStore.SecurityLevel.S1,
encrypt: false
};
| 参数 | 说明 |
|---|---|
name |
数据库文件名,存储在应用的沙箱目录下 |
securityLevel |
安全级别:S1(低)~ S4(高),S1 适合普通应用数据 |
encrypt |
是否加密,false 表示不加密 |
1.2 数据模型接口
typescript
export interface TodoItem {
id?: number;
date?: string;
dayOfWeek?: string;
time?: string;
content?: string;
isCompleted?: boolean;
}
export interface StatItem {
total?: number;
completed?: number;
uncompleted?: number;
}
TodoItem:待办事项模型,id为自增主键,isCompleted为布尔值。StatItem:统计模型,用于展示总任务数、已完成数和未完成数。
1.3 建表 SQL
typescript
const TABLE_NAME = 'todo_items';
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
dayOfWeek TEXT NOT NULL,
time TEXT NOT NULL,
content TEXT NOT NULL,
isCompleted INTEGER NOT NULL DEFAULT 0
)
`;
关键点:
IF NOT EXISTS:避免重复建表导致错误。AUTOINCREMENT:主键自动递增,插入时无需指定id。isCompleted INTEGER:SQLite 没有原生的 BOOLEAN 类型,使用INTEGER(0/1)存储布尔值。
2. 数据库初始化
typescript
export class TodoDatabase {
private store: relationalStore.RdbStore | null = null;
private context: Context;
constructor(context: Context) {
this.context = context;
}
async initialize(): Promise<void> {
try {
this.store = await relationalStore.getRdbStore(this.context, STORE_CONFIG);
await this.store.executeSql(CREATE_TABLE_SQL);
console.info('TodoDatabase initialized successfully');
} catch (error) {
console.error('Failed to initialize TodoDatabase:', error);
throw new Error('Failed to initialize TodoDatabase: ' + (error as BusinessError).message);
}
}
}
初始化流程:
getRdbStore:获取(或创建)数据库实例,传入Context和StoreConfig。executeSql:执行建表 SQL 语句。- 错误处理 :捕获异常并抛出带详细信息的
Error。
重要 :所有数据库操作前必须先调用
initialize(),否则this.store为null,后续操作会抛出异常。
3. 增:addTodo
typescript
async addTodo(todo: TodoItem): Promise<number> {
if (!this.store) {
throw new Error('Database not initialized');
}
try {
const valueBucket: relationalStore.ValuesBucket = {
date: todo.date!!,
dayOfWeek: todo.dayOfWeek!!,
time: todo.time!!,
content: todo.content!!,
isCompleted: todo.isCompleted ? 1 : 0
};
const rowId = await this.store.insert(TABLE_NAME, valueBucket);
console.info(`Todo added successfully, rowId: ${rowId}`);
return rowId;
} catch (error) {
throw new Error('Failed to add todo: ' + (error as BusinessError).message);
}
}
ValuesBucket:键值对容器,键为列名,值为要插入的数据。- 布尔值转换 :
todo.isCompleted ? 1 : 0,将 JavaScript 的boolean转为 SQLite 的INTEGER。 insert:执行插入操作,返回新行的rowId。
4. 查:多种查询方式
4.1 查询所有待办
typescript
async getAllTodos(): Promise<TodoItem[]> {
if (!this.store) {
throw new Error('Database not initialized');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.orderByDesc('date').orderByAsc('time');
const resultSet = await this.store.query(predicates);
const todos: TodoItem[] = [];
if (resultSet.rowCount > 0) {
resultSet.goToFirstRow();
do {
todos.push(this.resultSetToTodoItem(resultSet));
} while (resultSet.goToNextRow());
}
resultSet.close();
return todos;
}
RdbPredicates 是查询谓词构造器,支持链式调用:
orderByDesc('date'):按日期降序。orderByAsc('time'):按时间升序。
ResultSet 遍历模式:
goToFirstRow() → 读取数据 → goToNextRow() → 读取数据 → ... → 循环结束
必须关闭 ResultSet :每次查询后调用
resultSet.close()释放资源,否则会造成内存泄漏。
4.2 按条件查询
typescript
async getTodoById(id: number): Promise<TodoItem | null> {
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
const resultSet = await this.store.query(predicates);
if (resultSet.rowCount === 0) {
resultSet.close();
return null;
}
resultSet.goToFirstRow();
const todo = this.resultSetToTodoItem(resultSet);
resultSet.close();
return todo;
}
equalTo :等值条件,等价于 SQL 的 WHERE id = ?。
4.3 按日期查询
typescript
async getTodosByDate(date: string): Promise<TodoItem[]> {
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('date', date).orderByAsc('time');
// ... 遍历 resultSet
}
4.4 查询未完成待办
typescript
async getUncompletedTodos(): Promise<TodoItem[]> {
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('isCompleted', 0)
.orderByDesc('date')
.orderByAsc('time');
// ... 遍历 resultSet
}
注意查询条件使用 0 而非 false,因为数据库中布尔值存储为 INTEGER。
4.5 模糊搜索
typescript
async searchTodos(keyword: string): Promise<TodoItem[]> {
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.like('content', `%${keyword}%`)
.orderByDesc('date')
.orderByAsc('time');
// ... 遍历 resultSet
}
like :模糊匹配,%keyword% 等价于 SQL 的 WHERE content LIKE '%keyword%'。
5. 改:updateTodo 与 toggleTodoCompletion
5.1 动态更新
typescript
async updateTodo(id: number, todo: TodoItem): Promise<boolean> {
if (!this.store) {
throw new Error('Database not initialized');
}
const valueBucket: relationalStore.ValuesBucket = {};
if (todo.date !== undefined) valueBucket.date = todo.date;
if (todo.dayOfWeek !== undefined) valueBucket.dayOfWeek = todo.dayOfWeek;
if (todo.time !== undefined) valueBucket.time = todo.time;
if (todo.content !== undefined) valueBucket.content = todo.content;
if (todo.isCompleted !== undefined) valueBucket.isCompleted = todo.isCompleted ? 1 : 0;
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
const rowsAffected = await this.store.update(valueBucket, predicates);
return rowsAffected > 0;
}
动态 ValuesBucket :只设置需要更新的字段(undefined 检查),未设置的字段不会被更新。这样实现部分更新(PATCH 语义),而非全量覆盖。
5.2 切换完成状态
typescript
async toggleTodoCompletion(id: number): Promise<boolean> {
const todo = await this.getTodoById(id);
if (!todo) {
return false;
}
return await this.updateTodo(id, { isCompleted: !todo.isCompleted });
}
先读取当前状态,取反后更新------这是「读取-修改-写入」模式的典型实现。
6. 删:单条删除与批量删除
6.1 单条删除
typescript
async deleteTodo(id: number): Promise<boolean> {
if (!this.store) {
throw new Error('Database not initialized');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
const rowsAffected = await this.store.delete(predicates);
return rowsAffected > 0;
}
6.2 批量删除已完成项
typescript
async deleteCompletedTodos(): Promise<number> {
if (!this.store) {
throw new Error('Database not initialized');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('isCompleted', 1);
const rowsAffected = await this.store.delete(predicates);
return rowsAffected;
}
返回受影响的行数,UI 层可以据此显示「已清除 N 条已完成待办」。
7. 统计查询:COUNT 聚合
typescript
async getStatistics(): Promise<StatItem> {
if (!this.store) {
throw new Error('Database not initialized');
}
// 总数
const totalPredicates = new relationalStore.RdbPredicates(TABLE_NAME);
const totalResult = await this.store.query(totalPredicates, ['COUNT(*) as count']);
totalResult.goToFirstRow();
const total = totalResult.getLong(0);
totalResult.close();
// 已完成数
const completedPredicates = new relationalStore.RdbPredicates(TABLE_NAME);
completedPredicates.equalTo('isCompleted', 1);
const completedResult = await this.store.query(completedPredicates, ['COUNT(*) as count']);
completedResult.goToFirstRow();
const completed = completedResult.getLong(0);
completedResult.close();
return {
total,
completed,
uncompleted: total - completed
};
}
query(predicates, columns):第二个参数指定查询列,['COUNT(*) as count']执行聚合查询。getLong(0):按列索引读取整数值,0对应查询结果的第一列。- 两次查询分别获取总数和已完成数,未完成数通过计算得出。
8. ResultSet 转对象:resultSetToTodoItem
typescript
private resultSetToTodoItem(resultSet: relationalStore.ResultSet): TodoItem {
return {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
date: resultSet.getString(resultSet.getColumnIndex('date')),
dayOfWeek: resultSet.getString(resultSet.getColumnIndex('dayOfWeek')),
time: resultSet.getString(resultSet.getColumnIndex('time')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
isCompleted: resultSet.getLong(resultSet.getColumnIndex('isCompleted')) === 1
};
}
这是数据库行到业务对象的映射方法:
getColumnIndex:根据列名获取列索引,解耦列顺序依赖。getLong/getString:按类型读取列值。- 布尔值转换 :
getLong(...) === 1,将INTEGER还原为boolean。
9. 关闭数据库
typescript
async close(): Promise<void> {
if (this.store) {
await this.store.close();
this.store = null;
console.info('TodoDatabase closed');
}
}
在应用退出或不再需要数据库时调用,释放资源。
10. 布尔值存储技巧总结
由于 SQLite 不支持 BOOLEAN 类型,ArkData RDB 中使用 INTEGER 存储布尔值:
| 业务层 | 数据库 | 写入 | 读取 |
|---|---|---|---|
true |
1 |
isCompleted ? 1 : 0 |
getLong(...) === 1 |
false |
0 |
isCompleted ? 1 : 0 |
getLong(...) === 1 |
这种模式在关系型数据库开发中非常常见,务必牢记。
11. CRUD 方法速查表
| 方法 | SQL 等价 | 返回值 |
|---|---|---|
initialize() |
getRdbStore + CREATE TABLE |
void |
addTodo() |
INSERT INTO |
rowId: number |
getAllTodos() |
SELECT * ORDER BY |
TodoItem[] |
getTodoById() |
SELECT * WHERE id = ? |
`TodoItem |
getTodosByDate() |
SELECT * WHERE date = ? |
TodoItem[] |
getUncompletedTodos() |
SELECT * WHERE isCompleted = 0 |
TodoItem[] |
updateTodo() |
UPDATE SET ... WHERE id = ? |
boolean |
toggleTodoCompletion() |
SELECT → UPDATE |
boolean |
deleteTodo() |
DELETE WHERE id = ? |
boolean |
deleteCompletedTodos() |
DELETE WHERE isCompleted = 1 |
number |
searchTodos() |
SELECT * WHERE content LIKE ? |
TodoItem[] |
getStatistics() |
SELECT COUNT(*) |
StatItem |
close() |
store.close() |
void |
12. 总结
| 知识点 | 说明 |
|---|---|
@kit.ArkData |
关系型数据库 Kit |
StoreConfig |
数据库配置:文件名、安全级别、加密 |
getRdbStore |
获取数据库实例 |
executeSql |
执行原生 SQL |
ValuesBucket |
键值对,用于 INSERT/UPDATE |
RdbPredicates |
查询谓词:equalTo、like、orderBy |
ResultSet |
查询结果集:goToFirstRow、goToNextRow、getLong、getString |
getColumnIndex |
按列名取索引,解耦列顺序 |
INTEGER 0/1 |
SQLite 布尔值存储方案 |
resultSet.close() |
释放结果集资源 |
关系型数据库是移动端最可靠的数据持久化方案之一。掌握 RdbPredicates 的链式查询、ValuesBucket 的动态构建和 ResultSet 的安全遍历,就能应对绝大多数本地存储需求。