news 2026/3/11 21:03:08

【鸿蒙原生开发会议随记 Pro】 增删改查 封装一个优雅的 SQLite 数据库单例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【鸿蒙原生开发会议随记 Pro】 增删改查 封装一个优雅的 SQLite 数据库单例

文章目录

    • 一、 为什么必须是单例?
    • 二、 DAO 层与 CRUD 实战
    • 三、 在 EntryAbility 中激活它
    • 四、 总结

在上一篇文章中,我们像绘制建筑蓝图一样,设计了 Project、Contact、Meeting 三张核心表,并理清了它们之间错综复杂的关联关系。我们还决定抛弃简单的 UserPreferences,拥抱强大的 RelationalStore。但这仅仅是纸上谈兵。

现在的数据库还只是躺在代码文件里的几行 SQL 字符串,它既不能存数据,也不能查数据。

今天,我们要当一次泥瓦匠,把这些蓝图变成实实在在的房子。

对于大家来说,调用 API 打开数据库并不难。难的是如何优雅地管理这个数据库连接。你是否遇到过这样的情况:App 运行久了变得卡顿,排查发现是数据库连接打开了无数次却没关闭?或者 App 发布了 2.0 版本,用户更新后一打开就闪退,因为你加了新表却忘记处理旧数据的迁移?

这些都是烂代码埋下的雷。今天我们要写的代码,就是要排掉这些雷。我们要封装一个全局单例的 RdbManager,它不仅能安全地管理数据库连接,还能聪明地处理版本升级。

一、 为什么必须是单例?

在移动端开发中,数据库连接(RdbStore)是一个非常的对象。它对应着底层的文件句柄和内存缓冲区。如果你在每次点击保存按钮时都去getRdbStore,用完又忘了关,或者在多个页面同时持有多个连接实例,那么很快你的 App 就会因为资源耗尽而崩溃,或者遇到文件锁冲突导致写入失败。

所以,我们必须强制使用单例模式。这意味着,无论你的 App 有多少个页面,无论你何时何地需要查数据,你拿到的永远是同一个数据库连接实例。

让我们打开entry/src/main/ets/data/db/RdbManager.ts,对上一篇的雏形进行深度改造。这一次,我们要加入一个至关重要的逻辑:STORE_CONFIG的完整配置。

// entry/src/main/ets/data/db/RdbManager.ts import { relationalStore } from '@kit.ArkData'; import { common } from '@kit.AbilityKit'; import { DB_NAME, DB_SECURITY_LEVEL, SQL_CREATE_PROJECT, SQL_CREATE_CONTACT, SQL_CREATE_MEETING } from './MeetingRdb'; export class RdbManager { // 静态私有变量,持有唯一实例 private static instance: RdbManager; // 持有数据库操作对象 private rdbStore: relationalStore.RdbStore | null = null; // 应用上下文,初始化时注入 private context: common.UIAbilityContext | null = null; // 这里的版本号非常关键,后续升级全靠它 private static readonly DB_VERSION = 1; /** * 获取单例 * 无论调用多少次,返回的都是同一个 RdbManager 对象 */ public static getInstance(): RdbManager { if (!RdbManager.instance) { RdbManager.instance = new RdbManager(); } return RdbManager.instance; } /** * 初始化方法 * 建议在 EntryAbility 的 onCreate 中调用 * 这样 App 一启动,上下文就准备好了 */ public init(context: common.UIAbilityContext): void { this.context = context; // 可以在这里预热数据库,也可以懒加载 } /** * 核心方法:获取 RdbStore * 采用了“懒加载”策略,只有真正需要查数据时才打开连接 */ public async getRdbStore(): Promise<relationalStore.RdbStore> { // 1. 如果已经打开且未关闭,直接返回复用 if (this.rdbStore) { return this.rdbStore; } // 2. 检查 Context 是否注入 if (!this.context) { throw new Error('[RdbManager] Context is null! Call init() first.'); } // 3. 配置数据库参数 const config: relationalStore.StoreConfig = { name: DB_NAME, securityLevel: DB_SECURITY_LEVEL, // 开启加密(可选,商业级项目建议开启,这里暂不演示) // encrypt: false }; try { // 4. 调用系统 API 打开或创建数据库 this.rdbStore = await relationalStore.getRdbStore(this.context, config); // 5. 检查版本并升级 // 这是一个非常关键的步骤,决定了你的 App 能否平滑更新 if (this.rdbStore.version === 0) { // version 为 0 说明是全新安装,直接建表 await this.initTables(this.rdbStore); this.rdbStore.version = RdbManager.DB_VERSION; } else if (this.rdbStore.version < RdbManager.DB_VERSION) { // 这里的 version 是旧版本号,说明用户是覆盖安装 // 我们需要执行升级逻辑 await this.upgradeTables(this.rdbStore, this.rdbStore.version, RdbManager.DB_VERSION); this.rdbStore.version = RdbManager.DB_VERSION; } return this.rdbStore; } catch (e) { console.error(`[RdbManager] Get Store Failed: ${JSON.stringify(e)}`); // 这里可以抛出自定义异常,让 UI 层捕获并提示用户 throw new Error('Database init failed'); } } /** * 第一次建表逻辑 * 干净利落,把所有 CREATE TABLE 语句执行一遍 */ private async initTables(store: relationalStore.RdbStore) { console.info('[RdbManager] Initialize tables...'); // 使用事务可以加快批量执行速度 store.beginTransaction(); try { await store.executeSql(SQL_CREATE_PROJECT); await store.executeSql(SQL_CREATE_CONTACT); await store.executeSql(SQL_CREATE_MEETING); store.commit(); } catch (e) { console.error(`[RdbManager] Init tables failed: ${e}`); store.rollBack(); throw e; } } /** * 数据库升级逻辑 * 随着 App 版本迭代,这里会越来越长 */ private async upgradeTables(store: relationalStore.RdbStore, oldVersion: number, newVersion: number) { console.info(`[RdbManager] Upgrade DB from ${oldVersion} to ${newVersion}`); // 假设未来我们发布了 2.0 版本,DB_VERSION 变成了 2 // if (oldVersion < 2) { // // 在 Project 表加一个字段 "sort_order" // await store.executeSql('ALTER TABLE project ADD COLUMN sort_order INTEGER'); // } // 假设发布了 3.0 版本 // if (oldVersion < 3) { ... } } }

通过这段代码,我们把复杂的生命周期管理封装在了一个黑盒子里。对于调用者(比如 ViewModel)来说,它只需要await RdbManager.getInstance().getRdbStore(),就能拿到一个健康、可用、版本正确的数据库对象,完全不需要关心底下发生了什么。

二、 DAO 层与 CRUD 实战

有了管家,我们还需要工人。在架构设计中,我们通常会引入DAO(Data Access Object)层,或者叫Repository(仓库)层,专门负责具体的增删改查业务。我们不要把 SQL 语句散落在 App 的各个角落。

请在entry/src/main/ets/data/db目录下新建ProjectRepo.ts。我们以项目(Project)这个最基础的实体为例,来演示如何实现一个标准的 CRUD 流程。

这里有一个痛点:数据库 API 需要的是ValuesBucket(用于插入)和返回ResultSet(用于查询),而我们的业务层需要的是Project对象。中间的转换代码写起来非常枯燥,但必须写,而且要写得健壮。

我们先写插入(Create)

TypeScript

// entry/src/main/ets/data/db/ProjectRepo.ts import { relationalStore } from '@kit.ArkData'; import { Project } from '../models/Project'; import { RdbManager } from './RdbManager'; import { TABLE_PROJECT } from './MeetingRdb'; /** * 插入一个新项目 * @param project 业务对象 * @returns 插入的行 ID */ export async function insertProject(project: Project): Promise<number> { // 1. 获取数据库实例 const store = await RdbManager.getInstance().getRdbStore(); // 2. 构建 ValuesBucket // 这是鸿蒙 RdbStore 要求的特定格式,类似于一个 Map // 键是数据库列名,值是数据 // 注意:我们这里手动映射了驼峰属性到下划线列名 const valueBucket: relationalStore.ValuesBucket = { 'id': project.id, 'name': project.name, 'description': project.description || '', // 处理 undefined 'color': project.color, 'created_at': project.createdAt, 'updated_at': project.updatedAt }; // 3. 执行插入 // relationalStore.ConflictResolution.REPLACE 表示如果 ID 冲突则覆盖 // 这对于去重非常有用 const rowId = await store.insert(TABLE_PROJECT, valueBucket, relationalStore.ConflictResolution.REPLACE); return rowId; }

接下来是查询(Read)。这是最繁琐的部分,因为ResultSet是一个游标,我们需要遍历它,把每一列的数据取出来,填到Project对象里。

为了不让重复代码淹没我们,建议把“单行转对象”的逻辑抽离成一个 Helper 函数。

TypeScript

// entry/src/main/ets/data/db/ProjectRepo.ts /** * 查询所有项目 * 按更新时间倒序排列(最近活跃的在前) */ export async function queryAllProjects(): Promise<Project[]> { const store = await RdbManager.getInstance().getRdbStore(); // 1. 构建查询条件 // RdbPredicates 是鸿蒙提供的强大的查询构建器,类似 SQL 的 WHERE 子句 const predicates = new relationalStore.RdbPredicates(TABLE_PROJECT); predicates.orderByDesc('updated_at'); // 排序 // 2. 执行查询 const resultSet = await store.query(predicates); // 3. 遍历结果集 const projects: Project[] = []; try { // goToNextRow() 返回 false 表示游标走到头了 while (resultSet.goToNextRow()) { // 调用转换函数 projects.push(convertRowToProject(resultSet)); } } finally { // 4. 极其重要:关闭 ResultSet 释放内存! // 很多内存泄漏都是因为忘了这一步 resultSet.close(); } return projects; } /** * 辅助函数:将当前游标指向的行转换为 Project 对象 */ function convertRowToProject(resultSet: relationalStore.ResultSet): Project { // getColumnIndex 是为了防范列名写错或者列不存在的情况 // 虽然稍微损耗一点性能,但更安全 return { id: resultSet.getString(resultSet.getColumnIndex('id')), name: resultSet.getString(resultSet.getColumnIndex('name')), description: resultSet.getString(resultSet.getColumnIndex('description')), color: resultSet.getString(resultSet.getColumnIndex('color')), // getLong 对应数据库的 INTEGER createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')), updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')) }; }

你看,通过这种模式,无论 Project 表有多少列,复杂的取值逻辑都被封装在了convertRowToProject这一处。以后如果我们要给项目加个“图标”字段,只需要改这一个函数,而不用去改每一个查询方法。

最后简单带过更新(Update)删除(Delete)。它们的逻辑和查询类似,都是先构建RdbPredicates来锁定要操作的行。

TypeScript

// entry/src/main/ets/data/db/ProjectRepo.ts export async function updateProjectName(id: string, newName: string): Promise<number> { const store = await RdbManager.getInstance().getRdbStore(); const valueBucket: relationalStore.ValuesBucket = { 'name': newName, 'updated_at': Date.now() // 记得更新时间戳 }; const predicates = new relationalStore.RdbPredicates(TABLE_PROJECT); predicates.equalTo('id', id); // WHERE id = ? return store.update(valueBucket, predicates); } export async function deleteProject(id: string): Promise<number> { const store = await RdbManager.getInstance().getRdbStore(); const predicates = new relationalStore.RdbPredicates(TABLE_PROJECT); predicates.equalTo('id', id); return store.delete(predicates); }

三、 在 EntryAbility 中激活它

现在,我们的武器库已经准备好了,但它们还没被加载。我们需要在 App 启动的最早时机,把 Context 注入给RdbManager

打开entry/src/main/ets/entryability/EntryAbility.ts。这是 Stage 模型中 Ability 的生命周期入口。

// entry/src/main/ets/entryability/EntryAbility.ts import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; import { RdbManager } from '../data/db/RdbManager'; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 在 Ability 创建时,初始化数据库管理器 // 传入 context,这样 RdbManager 就能拿到沙箱路径 RdbManager.getInstance().init(this.context); console.info('[EntryAbility] RdbManager initialized'); } // ... 其他生命周期方法 }

这里有一个常识性的问题:为什么不在Index.etsaboutToAppear里初始化?

因为Index.ets是 UI 界面。如果你的 App 支持后台运行或者卡片(Service Widget)启动,有可能 UI 界面根本不需要显示,但后台服务需要读写数据库。

如果在 UI 里初始化,后台任务就会因为拿不到 Context 而崩溃。在EntryAbility初始化是目前最稳妥的做法。

四、 总结

今天,我们干了一件非常硬核的事情。

我们不仅写了代码,更是在构建基础设施。我们封装了一个线程安全、支持版本升级的RdbManager单例,彻底解决了数据库连接管理的后顾之忧。我们还实践了标准的 DAO 模式,将底层的ValuesBucketResultSet转换逻辑屏蔽在Repo文件内部,为上层的 ViewModel 提供了干净清爽的 TypeScript 接口。

现在的会议随记 Pro,已经具备了记忆能力。它不再是那个重启就会失忆的 Demo,而是一个能持久化存储用户资产的商业级应用雏形。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/10 18:36:48

图像矢量化完全指南:从入门到精通的高效转换技巧

图像矢量化完全指南&#xff1a;从入门到精通的高效转换技巧 【免费下载链接】SVGcode Convert color bitmap images to color SVG vector images. 项目地址: https://gitcode.com/gh_mirrors/sv/SVGcode 图像矢量化是将像素组成的位图转换为数学路径定义的矢量图形的过…

作者头像 李华
网站建设 2026/3/11 20:39:05

我用Unsloth三天学会模型微调,效果超出预期

我用Unsloth三天学会模型微调&#xff0c;效果超出预期 你有没有试过在显卡上跑一个微调任务&#xff0c;等了两小时发现显存爆了&#xff1f;或者改了十次参数&#xff0c;训练loss还是飘在天上&#xff1f;我之前也是这样——直到遇见Unsloth。 它不是又一个“理论上很美”…

作者头像 李华
网站建设 2026/3/9 11:38:22

verl安装成功标志是什么?看完就懂了

verl安装成功标志是什么&#xff1f;看完就懂了 verl 是一个专为大型语言模型&#xff08;LLMs&#xff09;后训练设计的强化学习&#xff08;RL&#xff09;训练框架&#xff0c;由字节跳动火山引擎团队开源&#xff0c;是 HybridFlow 论文的工程落地实现。它不是面向终端用户…

作者头像 李华
网站建设 2026/3/10 13:22:23

Qwen All-in-One如何快速上手?保姆级教程从零开始

Qwen All-in-One如何快速上手&#xff1f;保姆级教程从零开始 1. 这不是另一个AI工具&#xff0c;而是一次轻量级智能的重新定义 你有没有试过装完一个AI项目&#xff0c;发现光依赖包就下了半小时&#xff0c;显存爆满、环境报错、模型权重下载失败……最后连“Hello World”…

作者头像 李华
网站建设 2026/3/11 15:49:25

YOLO11开发环境快照分享,拿来就用

YOLO11开发环境快照分享&#xff0c;拿来就用 你是否还在为配置YOLO11训练环境反复踩坑&#xff1f;conda冲突、依赖版本打架、ultralytics源码编译失败、ONNX导出报错……这些本不该成为你验证新模型效果的门槛。今天不讲原理、不堆参数&#xff0c;只给你一个开箱即用的YOLO1…

作者头像 李华
网站建设 2026/3/11 15:49:23

Keil5在工业控制项目中处理中文注释的超详细版说明

以下是对您提供的博文内容进行 深度润色与专业重构后的版本 。我以一位深耕工业嵌入式开发十余年、常年带团队做电力/工控类项目的技术博主身份,用更自然、更具实战感的语言重写了全文—— 去AI味、增人味;删模板、留干货;弱理论、强落地;重逻辑、轻套路 。全文已彻底摒…

作者头像 李华