鸿蒙应用开发实战【31】--- CardDao 卡号增删改查完整实现
本文是「号码助手全栈开发系列」第 31 篇,持续更新中...
前言
前一篇我们定义了 CardEntity 和 CardColor 枚举,完成了数据模型层。现在进入 DAO(Data Access Object)层的完整实现------CardDao 封装了对 card 表的所有数据库操作。除了基础 CRUD,本篇还将涵盖批量操作、条件过滤、分页查询、模糊搜索等进阶能力。
本篇涵盖:基本 CRUD 回顾与完善、insert 返回自增 ID、条件过滤(按运营商/颜色筛选)、模糊搜索(LIKE 查询)、批量删除、分页查询(limit + offset)、Count 聚合查询、多个查询方法的完整组合。

一、CardDao 基础框架
1.1 类结构
typescript
import { relationalStore } from '@kit.ArkData';
import { CardEntity, CardColor } from '../entity/CardEntity';
export class CardDao {
private store: relationalStore.RdbStore;
constructor(store: relationalStore.RdbStore) {
this.store = store;
}
}
DAO 层通过构造方法注入 RdbStore。这种 依赖注入 模式让 DAO 不依赖 DatabaseService 单例,测试时也可以轻松传入 mock store。
1.2 列索引常量表
typescript
const COL_CARD = {
ID: 0,
LABEL: 1,
PHONE: 2,
OPERATOR: 3,
COLOR: 4,
SORT_ORDER: 5,
CREATED_AT: 6,
UPDATED_AT: 7,
} as const;
1.3 ResultSet 解析
typescript
function parseCard(rs: relationalStore.ResultSet): CardEntity {
return {
id: rs.getLong(COL_CARD.ID),
label: rs.getString(COL_CARD.LABEL),
phone: rs.getString(COL_CARD.PHONE),
operator: rs.getString(COL_CARD.OPERATOR) || undefined,
color: (rs.getString(COL_CARD.COLOR) as CardColor) || CardColor.BLUE,
sort_order: rs.getLong(COL_CARD.SORT_ORDER),
created_at: rs.getString(COL_CARD.CREATED_AT),
updated_at: rs.getString(COL_CARD.UPDATED_AT),
};
}
1.4 ValuesBucket 工具
typescript
function toValues(card: CardEntity): relationalStore.ValuesBucket {
return {
'label': card.label,
'phone': card.phone,
'operator': card.operator ?? null,
'color': card.color ?? CardColor.BLUE,
'sort_order': card.sort_order ?? 0,
};
}
二、基础 CRUD
2.1 插入
typescript
async insert(card: CardEntity): Promise<number> {
const rowId = await this.store.insert('card', toValues(card));
console.info(`CardDao.insert: id=${rowId}, label=${card.label}`);
return rowId;
}
insert 返回新记录的 rowId,对应 SQLite 的 last_insert_rowid()。
2.2 根据 ID 查询
typescript
async getById(id: number): Promise<CardEntity | null> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('id', id);
let rs = await this.store.query(predicates);
try {
if (rs.rowCount === 0) return null;
rs.goToFirstRow();
return parseCard(rs);
} finally {
rs.close();
}
}
✅ try-finally 模式 :
rs.close()在 finally 中执行,确保异常时也释放资源。
2.3 查询全部
typescript
async listAll(): Promise<CardEntity[]> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.orderByAsc('sort_order');
return await this.queryList(predicates);
}
2.4 更新
typescript
async update(card: CardEntity): Promise<number> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('id', card.id!);
// 同时更新 updated_at
const values = toValues(card);
values['updated_at'] = new Date().toISOString();
const changed = await this.store.update(values, predicates);
return changed;
}
2.5 删除
typescript
async delete(id: number): Promise<number> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('id', id);
const deleted = await this.store.delete(predicates);
console.info(`CardDao.delete: id=${id}, affected=${deleted}`);
return deleted;
}
三、条件过滤与搜索
3.1 按运营商过滤
typescript
async listByOperator(operator: string): Promise<CardEntity[]> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('operator', operator);
predicates.orderByAsc('sort_order');
return await this.queryList(predicates);
}
3.2 按颜色过滤
typescript
async listByColor(color: CardColor): Promise<CardEntity[]> {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('color', color);
return await this.queryList(predicates);
}
3.3 模糊搜索
typescript
/**
* 在 label、phone、operator 三个字段中搜索
*/
async search(keyword: string): Promise<CardEntity[]> {
let predicates = new relationalStore.RdbPredicates('card');
// label LIKE '%keyword%' OR phone LIKE '%keyword%' OR operator LIKE '%keyword%'
predicates.like('label', `%${keyword}%`);
predicates.or();
predicates.like('phone', `%${keyword}%`);
predicates.or();
predicates.like('operator', `%${keyword}%`);
predicates.orderByAsc('sort_order');
return await this.queryList(predicates);
}
使用示例:
typescript
// 搜索 "移动" → 匹配运营商为"中国移动"的卡、或标签含"移动"的卡
const results = await cardDao.search('移动');
⚠️ 性能注意 :
LIKE '%keyword%'无法使用索引,在数据量较大时会有性能问题。对于本系列的规模(几十到几百张卡),临时全表扫描完全可以接受。
3.4 多种条件组合
在实际的筛选面板中,用户可能同时选择多个过滤条件:
typescript
/**
* 组合筛选:运营商 + 颜色 + 搜索关键词(全部可选)
*/
async filter(options: {
operator?: string;
color?: CardColor;
keyword?: string;
}): Promise<CardEntity[]> {
let predicates = new relationalStore.RdbPredicates('card');
let hasCondition = false;
if (options.operator) {
if (hasCondition) predicates.and();
predicates.equalTo('operator', options.operator);
hasCondition = true;
}
if (options.color) {
if (hasCondition) predicates.and();
predicates.equalTo('color', options.color);
hasCondition = true;
}
if (options.keyword) {
if (hasCondition) predicates.and();
predicates.like('label', `%${options.keyword}%`);
// 实际项目可能还需要 OR phone/operator
}
predicates.orderByAsc('sort_order');
return await this.queryList(predicates);
}
四、批量操作
4.1 批量删除
typescript
async deleteMany(ids: number[]): Promise<number> {
if (ids.length === 0) return 0;
let predicates = new relationalStore.RdbPredicates('card');
predicates.in('id', ids);
const deleted = await this.store.delete(predicates);
console.info(`CardDao.deleteMany: ids=${ids.length}, affected=${deleted}`);
return deleted;
}
predicates.in('id', ids) 生成 WHERE id IN (1,2,3,...),一条 SQL 删除多条记录。
4.2 批量更新排序
typescript
async batchUpdateSortOrder(items: { id: number; sort_order: number }[]): Promise<void> {
try {
this.store.beginTransaction();
for (const item of items) {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('id', item.id);
await this.store.update({ 'sort_order': item.sort_order }, predicates);
}
this.store.commit();
} catch (error) {
this.store.rollback();
console.error('batchUpdateSortOrder failed', error);
throw error;
}
}
拖拽排序后用事务批量更新每条记录的 sort_order。
五、分页查询
当卡号数量较多时,使用分页提升列表渲染性能:
typescript
/**
* 分页查询
* @param page 页码,从 0 开始
* @param pageSize 每页条数
*/
async listPaged(page: number, pageSize: number): Promise<{
list: CardEntity[];
total: number;
}> {
// 先查总数
const total = await this.count();
// 再查分页数据
let predicates = new relationalStore.RdbPredicates('card');
predicates.orderByAsc('sort_order');
predicates.limitAs(pageSize);
predicates.offsetAs(page * pageSize);
const list = await this.queryList(predicates);
return { list, total };
}
总记录数查询
typescript
async count(): Promise<number> {
let predicates = new relationalStore.RdbPredicates('card');
let rs = await this.store.query(predicates, ['COUNT(*) AS count']);
try {
rs.goToFirstRow();
return rs.getLong(0);
} finally {
rs.close();
}
}
💡 列投影 :
query的第二个参数columns指定只查询COUNT(*)一个字段。比起SELECT *,这种方式减少数据传输量,性能更好。
六、聚合查询与统计
6.1 按运营商分组统计
typescript
async countByOperator(): Promise<{ operator: string; count: number }[]> {
// RdbPredicates 不支持 GROUP BY,使用 executeSql
const sql = `SELECT operator, COUNT(*) as count FROM card
WHERE operator IS NOT NULL
GROUP BY operator ORDER BY count DESC`;
let rs = await this.store.executeSql(sql);
// executeSql 不返回 ResultSet,此处仅为示意
// 实际需要直接使用 RdbStore 的 query 方法
return [];
}
📌 说明 :RdbPredicates 目前不支持
GROUP BY。本系列将在第 37 篇详细讲解通过executeSql+ 手动映射实现聚合查询的完整方案。
6.2 颜色分布统计
typescript
async countByColor(): Promise<Record<string, number>> {
const allCards = await this.listAll();
const colorMap: Record<string, number> = {};
for (const card of allCards) {
const color = card.color || CardColor.BLUE;
colorMap[color] = (colorMap[color] || 0) + 1;
}
return colorMap;
}
数据量不大时,可以内存中统计,代码更简洁。
七、通用查询方法
将重复的查询逻辑抽取为私有方法:
typescript
private async queryList(
predicates: relationalStore.RdbPredicates
): Promise<CardEntity[]> {
let rs = await this.store.query(predicates);
try {
let list: CardEntity[] = [];
while (rs.goToNextRow()) {
list.push(parseCard(rs));
}
return list;
} finally {
rs.close();
}
}
所有公开的 list*() 方法都基于此私有方法,确保 ResultSet 正确关闭。
八、完整 CardDao 类
将所有方法整合在一起:
typescript
export class CardDao {
private store: relationalStore.RdbStore;
constructor(store: relationalStore.RdbStore) {
this.store = store;
}
// 插入
async insert(card: CardEntity): Promise<number> {
const rowId = await this.store.insert('card', toValues(card));
return rowId;
}
// 根据 ID 查询
async getById(id: number): Promise<CardEntity | null> { /* ... */ }
// 查询全部
async listAll(): Promise<CardEntity[]> { /* ... */ }
// 按运营商过滤
async listByOperator(operator: string): Promise<CardEntity[]> { /* ... */ }
// 按颜色过滤
async listByColor(color: CardColor): Promise<CardEntity[]> { /* ... */ }
// 模糊搜索
async search(keyword: string): Promise<CardEntity[]> { /* ... */ }
// 组合筛选
async filter(options: { operator?: string; color?: CardColor; keyword?: string }): Promise<CardEntity[]> { /* ... */ }
// 分页查询
async listPaged(page: number, pageSize: number): Promise<{ list: CardEntity[]; total: number }> { /* ... */ }
// 计数
async count(): Promise<number> { /* ... */ }
// 颜色分布
async countByColor(): Promise<Record<string, number>> { /* ... */ }
// 更新
async update(card: CardEntity): Promise<number> { /* ... */ }
// 删除单条
async delete(id: number): Promise<number> { /* ... */ }
// 批量删除
async deleteMany(ids: number[]): Promise<number> { /* ... */ }
// 批量更新排序
async batchUpdateSortOrder(items: { id: number; sort_order: number }[]): Promise<void> { /* ... */ }
// 通用列表查询
private async queryList(predicates: relationalStore.RdbPredicates): Promise<CardEntity[]> { /* ... */ }
}
九、使用示例
typescript
// 在页面中获取 DAO
const store = await DatabaseService.getInstance().getStore(getContext(this));
const cardDao = new CardDao(store);
// 插入一张卡
const newId = await cardDao.insert({
label: '主卡',
phone: '13800138000',
operator: '中国移动',
color: CardColor.BLUE,
});
// 搜索
const results = await cardDao.search('移动');
// 分页
const { list, total } = await cardDao.listPaged(0, 20);
// 批量删除
await cardDao.deleteMany([1, 2, 3]);
小结
本篇完成了 CardDao 的完整进阶实现:
| 能力 | 方法 | 关键点 |
|---|---|---|
| 基础 CRUD | insert / getById / update / delete | 依赖注入 + try-finally |
| 条件过滤 | listByOperator / listByColor | RdbPredicates 精确匹配 |
| 模糊搜索 | search | LIKE %keyword% 三字段联查 |
| 组合筛选 | filter | AND 逻辑动态组合条件 |
| 批量删除 | deleteMany | predicates.in() |
| 批量更新 | batchUpdateSortOrder | 事务保护 |
| 分页查询 | listPaged | limitAs + offsetAs |
| 聚合统计 | count / countByColor | 列投影 / 内存统计 |
| 状态码说明 | 返回值含义 | 调用方处理指南 |
31.1 CardDao 状态码说明
| 返回值 | 含义 | 场景 |
|---|---|---|
> 0 |
成功,返回受影响行数 | insert / update / delete |
0 |
无影响(如空数组) | deleteMany(\[\]) |
null |
未找到记录 | getById 查询不存在 |
[] |
空列表 | listAll / search 无匹配 |
调用方应根据返回值类型做出相应处理:查询型方法判 null/空数组,命令型方法判受影响行数。
下一篇将实现「AppBindingDao 应用绑定 CRUD 实现」,涵盖 JOIN 查询、状态机驱动的 DAO 操作。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- openHarmony 跨平台社区:https://openharmonycrossplatform.csdn.net
- HarmonyOS 官方文档:https://developer.huawei.com/consumer/cn/doc/
- HarmonyOS RDB开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-rdb
- HarmonyOS hilog日志:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hilog