鸿蒙应用开发实战【31】— CardDao 卡号增删改查完整实现

鸿蒙应用开发实战【31】--- CardDao 卡号增删改查完整实现

本文是「号码助手全栈开发系列」第 31 篇,持续更新中...

开源社区:https://openharmonycrossplatform.csdn.net


前言

前一篇我们定义了 CardEntityCardColor 枚举,完成了数据模型层。现在进入 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 操作。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
全堆鸿蒙1 小时前
鸿蒙应用开发实战【36】— 数据查询优化:predicates 链式调用
数据库·harmonyos·鸿蒙系统
特立独行的猫a1 小时前
HarmonyOS鸿蒙PC三方库移植:Node.js 三方库鸿蒙 PC 移植指南
华为·node.js·node·harmonyos·三方库移植·鸿蒙pc
心中有国也有家1 小时前
AtomGit Flutter 鸿蒙客户端:错误处理与优雅降级策略
android·javascript·flutter·华为·harmonyos
youtootech10 小时前
HarmonyOS《柚兔学伴》项目实战09-待办列表 UI——List+ForEach+滑动删除
数据结构·ui·华为·list·harmonyos
不羁的木木10 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第6篇:图片滤镜效果实现
图像处理·深度学习·harmonyos
拥抱太阳061611 小时前
HarmonyOS 应用开发《掌上英语》第9篇—ArkTS @ObservedV2 + @Trace 响应式编程精讲
华为·harmonyos
拥抱太阳061611 小时前
HarmonyOS 应用开发《掌上英语》第10篇——@Local vs @Param vs @Trace 组件状态管理三剑客
华为·harmonyos
春卷同学17 小时前
HarmonyOS掌上记账APP开发实践第8篇:构建可测试的鸿蒙应用 — MoneyTrack 的自动化测试体系
华为·harmonyos