数据库 CRUD 与查询封装-Cordova 与 OpenHarmony 混合开发实战

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应项目模块:
cordova/src/main/resources/rawfile/www/js/db.js中底部那一段「通用 CRUD 和查询方法」,以及它们在上层业务中的用法。
1. 为什么要单独写一篇 CRUD 封装?
在上一篇模块 02 里,我们主要从「数据库结构」的角度,介绍了 FinanceDatabase 是如何用 6 张表承载整个财务管家的数据世界的。但有了表结构,还不够,你还需要一整套 好用、稳定、可复用的 CRUD 封装,否则每次操作数据都会写一大堆重复的 IndexedDB 事务代码。
这个模块的核心目标只有一个:
把所有对 IndexedDB 的读写,统一收敛到少数几个通用方法里,让上层业务代码几乎不用关心事务、store、index 等底层细节。
从长期维护来看,这种封装方式能显著减少 bug,也方便你在将来替换存储实现(比如迁到云端或加一层同步)。
2. 通用查询:getAll 与 getByIndex
先看两个最常用的查询封装,它们几乎支撑了整个应用的读操作:
/**
* 通用方法:获取所有记录
*/
async getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* 通用方法:按索引查询
*/
async getByIndex(storeName, indexName, value) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(value);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
2.1 getAll:最简单的批量读取
getAll 的设计非常直接:
- 入参只有一个
storeName,表示要从哪张“表”(objectStore)里取数据; - 内部开启一个
readonly事务,确保不会修改数据; - 调用
store.getAll()一次性取出所有记录; - 用 Promise 包装,让调用方可以用
await写法,简化异常处理逻辑。
在业务代码中,你会频繁看到类似用法:
async getAccounts() {
return this.getAll('accounts');
}
async getAllTransactions() {
return this.getAll('transactions');
}
也就是说,上层根本不需要知道 IndexedDB 的存在,只要调用 getAccounts() 就能拿到一个数组,像操作普通 JS 数据一样继续处理即可。
2.2 getByIndex:按索引查询的统一入口
getByIndex 则是对 IndexedDB 索引查询的统一封装:
storeName指定表;indexName指定使用哪个索引(例如type、month、accountId);value是索引匹配值;- 内部通过
store.index(indexName).getAll(value)取出所有符合条件的记录。
典型用法包括:
// 按账户获取交易
async getTransactionsByAccount(accountId) {
return this.getByIndex('transactions', 'accountId', accountId);
}
// 获取某个月的预算
async getMonthBudget(year, month) {
const monthKey = `${year}-${String(month).padStart(2, '0')}`;
const budgets = await this.getByIndex('budgets', 'month', monthKey);
return budgets.length > 0 ? budgets[0] : null;
}
// 按类型获取分类(收入/支出)
async getCategoriesByType(type) {
return this.getByIndex('categories', 'type', type);
}
可以看到,这三个高层方法都没有直接操作事务和索引,所有“底层活”都交给了 getByIndex:
- 你只需要记住每张表有哪些索引,就能很方便地在业务代码里做各种组合查询;
- 将来如果某个索引实现需要调整(例如换成范围查询),只需要修改
getByIndex一处,不必全项目搜一遍。
3. 通用写入:add、update、delete
再看写操作的三个通用方法:
/**
* 通用方法:添加记录
*/
async add(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(data);
});
}
/**
* 通用方法:更新记录
*/
async update(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(data);
});
}
/**
* 通用方法:删除记录
*/
async delete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
3.1 add:插入新记录
add 用来插入新数据,它默认要求传入的 data 已经包含主键字段(例如 id),这也是为什么在高层方法中经常会看到:
async addAccount(account) {
account.id = this.generateId();
account.createdAt = new Date().toISOString();
return this.add('accounts', account);
}
async addTransaction(transaction) {
transaction.id = this.generateId();
transaction.createdAt = new Date().toISOString();
return this.add('transactions', transaction);
}
这里有一个设计上的小细节:
- 生成主键 ID 的逻辑统一放在数据库层的
generateId()里,而不是散落在各个页面; - 这保证了所有表的主键风格一致,也方便后面做数据导出 / 导入时进行比对。
3.2 update:幂等更新
update 使用的是 store.put(data),而不是 add,意味着:
- 如果主键已存在,则更新记录;
- 如果主键不存在,则当作新记录插入。
这样做的一个直接收益是:在做数据导入时,你可以直接用 update 把备份文件里的每条记录写回数据库,而不用在意它是“新增”还是“覆盖”,逻辑十分干净。
3.3 delete:统一删除入口
delete 封装了从指定 store 删除某个 key 的逻辑:
async deleteAccount(accountId) {
return this.delete('accounts', accountId);
}
async deleteTransaction(transactionId) {
return this.delete('transactions', transactionId);
}
async deleteGoal(goalId) {
return this.delete('goals', goalId);
}
对上层来说,删除操作只需要知道主键 ID 即可,无需每次写一遍事务和对象仓库的样板代码。
4. ID 生成与清库:辅助能力
除了 CRUD,还有两个辅助能力值得一提:
/**
* 生成唯一ID
*/
generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 清空数据库
*/
async clear() {
const stores = ['accounts', 'transactions', 'categories', 'budgets', 'goals', 'tags'];
for (const storeName of stores) {
await new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
console.log('[DB] Database cleared');
}
generateId使用时间戳 + 随机串的组合,只要在单机环境中,基本能保证主键不冲突;clear则是一次性清空所有表的工具方法,在“清空所有数据”、“恢复出厂设置”等场景中非常有用,上层只需要调用await financeDB.clear()即可。
5. ArkTS 视角:为什么要把 CRUD 做成这样?
从 ArkTS 和整体架构的角度看,这套 CRUD 封装还有一个隐藏好处:
- 你的 ArkTS 插件(比如
FileManagerPlugin)完全不用关心数据库结构; - 插件只负责“文件层”的导入导出,真正的数据落库逻辑仍然由
db.js处理; - 这使得原生插件与业务数据表结构之间 弱耦合,未来即使调整 IndexedDB 的实现,也不需要修改 ArkTS 侧代码。
举个简单的链路例子:
- 用户在设置页点击“导出数据”;
- JS 调用
window.financeDB.exportData(),内部用getAll把 6 张表的数据全部导出; - JS 再调用
cordova.exec('FileManager', 'exportData', [...]),把这份 JSON 发给 ArkTS; - ArkTS 插件只负责把这块 JSON 写到用户选择的目录,不关心里面的字段结构;
- 将来导入时,ArkTS 插件从文件读回 JSON,交给 JS,JS 再调用
financeDB.importData(),内部用update写回各个 store。
在这个流程中,通用 CRUD 封装就像是一个「稳定的地基」,让原生层与 Web 层各司其职,又通过简单清晰的接口组合在一起。
6. 小结:这套 CRUD 封装的几个实践经验
综合来看,这一小段看似不起眼的 CRUD 封装,其实蕴含了不少可以复用到其他项目的经验:
- 用 Promise 包装 IndexedDB 事件,统一和 async/await 对齐,彻底告别回调地狱;
- 把所有 CRUD 操作收敛到少数几个通用方法,上层只通过表名和数据对象来调用,大幅减少样板代码;
- 高层方法按业务划分,例如
getAccounts、getMonthBudget、getTransactionsByAccount等,让页面逻辑只面对业务语义,而不是技术细节; - 主键生成与时间戳统一在数据库层处理,避免各个页面重复造轮子,也避免出现风格不一致的问题;
- 导出/导入接口围绕这套 CRUD 搭建,使得备份与恢复逻辑既简单又可靠;
- 与 ArkTS 插件保持弱耦合,让原生层专注系统能力,Web 层专注业务与数据结构。
在后续的模块中,无论是交易列表、预算管理,还是报表分析、趋势分析,所有数据读写都会不断复用到这套 CRUD 和查询封装。可以说,只要你把这个模块吃透,整个项目的数据读写路径就已经掌握了七八成。",“EmptyFile”:false}]} -> any}
更多推荐



所有评论(0)