HmarkX(码笺)的本地数据库模块重构实战
HmarkX(码笺)的本地数据库模块重构。读完你应该能回答:这个模块是干嘛的?为什么这么拆?怎么用?
1. 这个模块解决了什么问题
鸿蒙的关系型数据库 relationalStore(RDB)是一套底层 API:
- 要自己打开数据库
- 要自己拼 SQL
- 要自己
goToNextRow()/getString()解析 ResultSet - 要自己
close()释放资源 - 错误处理散落在每个
.then().catch()里
2. 设计目标
| 目标 | 怎么做到 |
|---|---|
| 数据库只打开一次 | DatabaseManager 单例 + 一次性初始化 |
| 业务代码看不到 SQL 细节 | 每张表配一个 Repository,对外只暴露语义化方法 |
| 业务代码只跟 model 打交道 | Repository 入参 = model 对象,内部转 ValuesBucket |
| 数据库层不碰 UI | 不传 UIContext,不弹 toast;toast 由调用方自己决定 |
| 风格统一 | 全部 async/await;错误统一 Logger.error + throw new Error(...) |
| 加表不改老代码 | 一张表 = 一个 Repository 文件 + 一个建表 SQL |
3. 三个角色
┌──────────────────────┐
│ DatabaseManager │ 单例:负责打开 / 持有 RdbStore
│ (开门人) │
└──────────┬───────────┘
│ getStore()
▼
┌──────────────────────┐
│ MdRepository │
│ (md 表 干活的小哥) │
└──────────────────────┘
▲
│
业务代码
类比:
- DatabaseManager = 仓库管理员。整个仓库(数据库)只有一把钥匙在他手里,谁要进仓库都得先报到(
init)。 - Repository = 货架管理员。每个货架(表)有专人,他熟悉自己那张表的所有字段,对外只说人话(“加一条”、“删一条”)。
3.1 DatabaseManager(数据库管理类)
import { relationalStore } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { Logger } from 'constant'
// 数据库文件名
const DB_NAME = 'hmmarkdown.db'
// 笔记表建表语句
const CREATE_MD_TABLE = `
create table if not exists md( mdId integer primary key autoincrement, title text, content text, createTime integer, updateTime integer, createYear integer )`
/**
* 数据库管理类(单例)
* 负责打开 / 持有 RdbStore,并对外暴露执行原始 SQL 的能力。
* 各业务表的具体 CRUD 由对应 Repository 完成。
*/export class DatabaseManager {
// 单例实例
private static instance: DatabaseManager | null = null
// RDB 实例,未初始化时为 null private store: relationalStore.RdbStore | null = null
// 是否正在初始化,避免并发重复初始化
private initializing: boolean = false
private constructor() {
}
/**
* 获取单例实例
*/
static getInstance(): DatabaseManager {
if (!DatabaseManager.instance) {
DatabaseManager.instance = new DatabaseManager()
}
return DatabaseManager.instance
}
/**
* 获取已初始化的 RdbStore,如果尚未初始化返回 null * 主要供 Repository 内部使用
*/
getStore(): relationalStore.RdbStore | null {
return this.store
}
/**
* 初始化数据库
* - 已初始化则直接返回
* - 正在初始化中(并发场景)也直接返回,调用方需自行确保使用前 await init * - 首次创建(version === 0)时建表
* @param context UIAbility 上下文
*/
async init(context: common.UIAbilityContext): Promise<void> {
// 已经初始化完成或正在初始化中,直接返回
if (this.store || this.initializing) {
return
}
this.initializing = true
try {
const storeConfig: relationalStore.StoreConfig = {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S3
}
const rdbStore = await relationalStore.getRdbStore(context, storeConfig)
this.store = rdbStore
// 首次创建数据库时执行建表
if (rdbStore.version === 0) {
await this.execute(CREATE_MD_TABLE)
await this.execute(CREATE_DOCSCAN_TABLE)
Logger.info('数据库初始化完成')
}
} catch (e) {
const err = e as BusinessError
Logger.error(`DatabaseManager init error ${err.code} ${err.message}`)
throw new Error(`DatabaseManager init error ${err.code} ${err.message}`)
} finally {
this.initializing = false
}
}
/**
* 执行任意 SQL 语句(建表、变更结构等)
* @param sql 待执行的 SQL */ async execute(sql: string): Promise<void> {
if (!this.store) {
Logger.error('DatabaseManager store 不存在')
throw new Error('DatabaseManager store not initialized')
}
try {
await this.store.executeSql(sql)
} catch (e) {
const err = e as BusinessError
Logger.error(`execute sql error ${err.code} ${err.message}`)
throw new Error(`execute sql error ${err.code} ${err.message}`)
}
}}
为什么是单例:
- RdbStore 是有状态的资源,重复打开会浪费内存,还可能并发冲突。
- 全应用共享一个连接,启动时初始化一次,后续 Repository 直接复用。
它对外只做三件事:
| 方法 | 作用 |
|---|---|
init(context) |
打开数据库;首次创建(version === 0)时自动建表 |
getStore() |
提供底层 RdbStore(仅 Repository 内部用,业务别调) |
execute(sql) |
执行任意 SQL(建表、改表结构等场景才用) |
并发安全的小细节:
initializing标志位防止并发场景下被同时初始化两次。- 后到的调用方挂在
pendingResolvers队列里,首次init完成后被一次性唤醒,没有轮询,没有重复打开。
3.2 MdRepository(md 笔记表)
| 方法 | 作用 |
|---|---|
insert(data: mdData) |
新建一条笔记(mdId 由数据库自增) |
update(data: mdData) |
用完整 mdData 更新,主键取 data.mdId |
delete(mdId: number) |
按主键删除 |
query(sql?) |
查询列表,默认按 mdId 倒序 |
searchByTitle(title) |
标题模糊搜索 |
入参直接是 model 对象。Repository 内部有一个私有 toValuesBucket(data, includeId?) 把 model 转成 RDB 需要的 ValuesBucket:
- 插入时
includeId = false,让数据库自增主键。 - 业务代码再也不用手写字段名,加字段时只动 model + Repository。
4. 为什么用单例?
新手可能会问:为什么不每次 new MdRepository()?
- Repository 本身没有状态(store 是从
DatabaseManager拿的),多个实例除了浪费内存别无意义。 - 单例可以全局直接
MdRepository.getInstance()调用,不用层层传参。 - 配合
DatabaseManager的单例,整条调用链都很轻量。
5. 为什么全部用 async / await,不用 Promise 写法?
老代码里有大量 return new Promise((resolve, reject) => { ... }),看起来像下面这样:
insert(): Promise<void> {
return new Promise((resolve, reject) => {
this.store.insert(...).then(() => resolve()).catch((e) => reject(e))
})
}
它的问题:
- 多包了一层 Promise,对 thenable 的 API 来说完全是冗余。
- 出错时
reject()经常忘带参数,调用方拿不到错误信息。 - 嵌套场景下层级会变深,可读性很差。
async/await 等价但更简洁:
async insert(): Promise<void> {
try {
await this.store.insert(...)
} catch (e) {
const err = e as BusinessError
Logger.error(`md insert error ${err.code} ${err.message}`)
throw new Error(`md insert error ${err.code} ${err.message}`)
}
}
注意:
async函数的返回类型必须是Promise<T>,这是语法层面的要求,并不是显式构造 Promise。这两件事不是一回事。
6. 错误处理为什么这么写?
每个 catch 都长这样:
} catch (e) {
const err = e as BusinessError
Logger.error(`xxx error ${err.code} ${err.message}`)
throw new Error(`xxx error ${err.code} ${err.message}`)
}
为什么这样:
- Logger.error:把原始错误信息打印出来,方便排查。
- throw new Error(…):往上层抛一个普通 Error,这样调用方在自己的 try/catch 里拿到的是稳定的
Error类型,而不是各种奇怪的 BusinessError / 字符串混杂。 - 不吞掉错误:除了 query(查空数据返回空数组算正常)以外,其它写操作出错一律抛出,让 UI 决定如何提示。
7. 怎么用
7.1 应用启动时
在 EntryAbility.onWindowStageCreate 或第一个用到数据库的页面 aboutToAppear 里:
import { DatabaseManager } from "database"
import { common } from "@kit.AbilityKit"
const context = this.getUIContext().getHostContext() as common.UIAbilityContext
await DatabaseManager.getInstance().init(context)
init多调几次没关系,重复调用直接复用已有连接,不会重复打开数据库。
7.2 增删改查 md 笔记
import { MdRepository, mdData } from "database"
import { toastUtil } from "utils"
const md = MdRepository.getInstance()
// 新增:mdId 由数据库自增,传 0 占位即可
const now = getTime()
const newDoc = new mdData("标题", "正文", now, now, getYear(), 0)
await md.insert(newDoc)
toastUtil(this.getUIContext(), "保存成功")
// 更新:用一个完整的 mdData,主键从 data.mdId 取
const updated = new mdData("新标题", "新正文", oldData.createTime, getTime(), oldData.createYear, oldData.mdId)
await md.update(updated)
toastUtil(this.getUIContext(), "修改成功")
// 删除:直接传主键
await md.delete(mdId)
toastUtil(this.getUIContext(), "删除成功")
// 查询
const list: mdData[] = await md.query()
const searchResult: mdData[] = await md.searchByTitle("关键字")
toast 不再由 Repository 负责,因为数据库层不应该关心 UI;调用方根据业务自己决定要不要弹提示。
8. 想新加一张表?
照着 MdRepository / 抄一份,三步走:
- 在
model/下新增表对应的数据类。 - 在
DatabaseManager顶部加上CREATE_XXX_TABLE建表 SQL,并在init里调一次await this.execute(CREATE_XXX_TABLE)。 - 在
database/下新增XxxRepository.ets,复制其它 Repository 改下表名 / 主键 / 字段映射。 - 把新类写进
Index.ets导出。
业务代码侧则继续用熟悉的 XxxRepository.getInstance().xxx() 调用即可,没有任何额外的初始化负担。
9. 一句话总结
DatabaseManager管开门,Repository管干活。 业务代码只跟 Repository 打交道,写出来的代码就是"我要干嘛",而不是"我要怎么操作 RDB"。
更多推荐




所有评论(0)