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()

  1. Repository 本身没有状态(store 是从 DatabaseManager 拿的),多个实例除了浪费内存别无意义。
  2. 单例可以全局直接 MdRepository.getInstance() 调用,不用层层传参
  3. 配合 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))
  })
}

它的问题:

  1. 多包了一层 Promise,对 thenable 的 API 来说完全是冗余。
  2. 出错时 reject() 经常忘带参数,调用方拿不到错误信息。
  3. 嵌套场景下层级会变深,可读性很差。

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 / 抄一份,三步走:

  1. model/ 下新增表对应的数据类。
  2. DatabaseManager 顶部加上 CREATE_XXX_TABLE 建表 SQL,并在 init 里调一次 await this.execute(CREATE_XXX_TABLE)
  3. database/ 下新增 XxxRepository.ets,复制其它 Repository 改下表名 / 主键 / 字段映射。
  4. 把新类写进 Index.ets 导出。

业务代码侧则继续用熟悉的 XxxRepository.getInstance().xxx() 调用即可,没有任何额外的初始化负担


9. 一句话总结

DatabaseManager 管开门,Repository 管干活。 业务代码只跟 Repository 打交道,写出来的代码就是"我要干嘛",而不是"我要怎么操作 RDB"。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐