HarmonyOS《柚兔学伴》项目实战08-关系型数据库入门——待办事项持久化

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);
    }
  }
}

初始化流程:

  1. getRdbStore :获取(或创建)数据库实例,传入 ContextStoreConfig
  2. executeSql:执行建表 SQL 语句。
  3. 错误处理 :捕获异常并抛出带详细信息的 Error

重要 :所有数据库操作前必须先调用 initialize(),否则 this.storenull,后续操作会抛出异常。


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() SELECTUPDATE 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 的安全遍历,就能应对绝大多数本地存储需求。

相关推荐
解局易否结局6 小时前
ArkTS 设计模式与架构进阶:构建可维护的 HarmonyOS 应用
华为·设计模式·架构·harmonyos
解局易否结局6 小时前
鸿蒙原生开发实战|Native JSON 编解码与高性能数据序列化
华为·json·harmonyos
Helen_cai6 小时前
HarmonyOS ArkTS 实战:实现一个校园门禁与访客预约应用
深度学习·华为·harmonyos
微三云 - 廖会灵 (私域系统开发)6 小时前
电商系统国际化架构设计:多语言、多币种、多时区、多税制的全链路实现
java·前端·数据库
周杰伦的稻香7 小时前
PostgreSQL中的METHOD(认证方法)
数据库·postgresql
不羁的木木7 小时前
HarmonyOS技术精讲-Connectivity Kit:蓝牙进阶——GATT服务与音频通道
华为·音视频·harmonyos
Helen_cai7 小时前
HarmonyOS ArkTS 实战:实现一个校园快递代取与跑腿应用
华为·harmonyos
万岳科技系统开发7 小时前
智慧医院小程序开发满足医疗机构多场景服务需求
数据库·学习·小程序
2301_768103497 小时前
HarmonyOS趣味相机实战第13篇:CameraPicker与CameraKit选型、系统拍照回退及架构边界
harmonyos·arkts·架构设计·camerapicker·camerakit