鸿蒙应用开发实战【37】— 数据统计:countByStatus 聚合查询

鸿蒙应用开发实战【37】--- 数据统计:countByStatus 聚合查询

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

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


前言

号码助手首页顶部有四个统计卡片------全部、使用中、待处理、已停用 。这些数字不是硬编码的占位符,而是从 app_binding 表中实时查询出来的。本篇将实现 countByStatus 聚合查询,涵盖 RDB 中聚合函数的应用、状态分布统计、以及首页统计卡片的数据驱动。

本篇涵盖:COUNT 聚合函数应用、基于 predicates 的 count 方法、状态分布统计 countByStatus、跨表统计(绑定数/卡号数)、首页统计卡片的数据联动、缓存与刷新策略。


一、COUNT 基础

1.1 基本计数

typescript 复制代码
async count(): Promise<number> {
  let predicates = new relationalStore.RdbPredicates('app_binding');
  let rs = await this.store.query(predicates, ['COUNT(*) AS count']);
  try {
    rs.goToFirstRow();
    return rs.getLong(0);
  } finally {
    rs.close();
  }
}

等价 SQL:SELECT COUNT(*) AS count FROM app_binding

1.2 带条件的计数

typescript 复制代码
async countByStatus(status: string): Promise<number> {
  let predicates = new relationalStore.RdbPredicates('app_binding');
  predicates.equalTo('status', status);
  let rs = await this.store.query(predicates, ['COUNT(*) AS count']);
  try {
    rs.goToFirstRow();
    return rs.getLong(0);
  } finally {
    rs.close();
  }
}

等价 SQL:SELECT COUNT(*) AS count FROM app_binding WHERE status = '使用中'


二、多个状态同时统计

每次查询一个计数,需要 4 次数据库查询。优化后一次查出所有状态分布:

2.1 方法一:多次查询(简单易懂)

typescript 复制代码
async getStatusStats(): Promise<{
  total: number;
  active: number;
  pending: number;
  disabled: number;
}> {
  const total = await this.count();
  const active = await this.countByStatus('使用中');
  const pendingSwitch = await this.countByStatus('待换绑');
  const pendingCancel = await this.countByStatus('待注销');
  const disabled = await this.countByStatus('已停用');

  return {
    total,
    active,
    pending: pendingSwitch + pendingCancel,
    disabled,
  };
}

优点: 代码直观,每个 DAO 方法职责单一

缺点: 4 次数据库查询,性能开销大

2.2 方式二:一次查询 + 内存统计

typescript 复制代码
async getStatusStats(): Promise<{
  total: number;
  active: number;
  pending: number;
  disabled: number;
}> {
  // 一次查出所有记录
  const all = await this.listAll();

  // 内存中统计
  let active = 0;
  let pending = 0;
  let disabled = 0;

  for (const item of all) {
    switch (item.status) {
      case '使用中':
        active++;
        break;
      case '待换绑':
      case '待注销':
        pending++;
        break;
      case '已停用':
        disabled++;
        break;
    }
  }

  return {
    total: all.length,
    active,
    pending,
    disabled,
  };
}

优点: 只需 1 次查询

缺点: 如果只需要计数,查询所有列浪费带宽

适用: 数据量不大(<1000)的场景

2.3 方式三:按状态多次 COUNT(推荐)

typescript 复制代码
async countByStatusGroup(): Promise<Record<string, number>> {
  const statuses = ['使用中', '待换绑', '待注销', '已停用'];
  const result: Record<string, number> = {};

  for (const status of statuses) {
    result[status] = await this.countByStatus(status);
  }

  return result;
}

优点: 每次只查一列 COUNT 值,数据传输最少

缺点: 多次查询,但有索引后每次是 O(log n)


三、跨表统计

首页统计卡片不仅显示绑定数,还可能显示卡号总数、候选记录数:

typescript 复制代码
export class DashboardStats {
  private cardDao: CardDao;
  private bindingDao: AppBindingDao;
  private candidateDao: SmsCandidateDao;

  constructor(store: relationalStore.RdbStore) {
    this.cardDao = new CardDao(store);
    this.bindingDao = new AppBindingDao(store);
    this.candidateDao = new SmsCandidateDao(store);
  }

  /**
   * 获取首页仪表盘的所有统计数据
   */
  async getAllStats(): Promise<{
    // 卡号统计
    totalCards: number;

    // 绑定统计
    totalBindings: number;
    activeBindings: number;
    pendingBindings: number;
    disabledBindings: number;

    // 候选统计
    pendingCandidates: number;
  }> {
    // 并行执行所有查询
    const [
      totalCards,
      totalBindings,
      activeBindings,
      pendingBindings,
      disabledBindings,
      pendingCandidates,
    ] = await Promise.all([
      this.cardDao.count(),
      this.bindingDao.count(),
      this.bindingDao.countByStatus('使用中'),
      (async () => {
        const s = await this.bindingDao.countByStatus('待换绑');
        const c = await this.bindingDao.countByStatus('待注销');
        return s + c;
      })(),
      this.bindingDao.countByStatus('已停用'),
      this.candidateDao.countUnimported(),
    ]);

    return {
      totalCards,
      totalBindings,
      activeBindings,
      pendingBindings,
      disabledBindings,
      pendingCandidates,
    };
  }
}

⚡ Promise.all 并行化 :所有 COUNT 查询互不依赖,可以并行执行。Promise.all 比串行快 3-4 倍。


四、首页统计卡片的数据驱动

4.1 状态定义

typescript 复制代码
@Entry
@Component
struct HomePage {
  @State stats: {
    totalCards: number;
    totalBindings: number;
    activeBindings: number;
    pendingBindings: number;
    disabledBindings: number;
    pendingCandidates: number;
  } = {
    totalCards: 0,
    totalBindings: 0,
    activeBindings: 0,
    pendingBindings: 0,
    disabledBindings: 0,
    pendingCandidates: 0,
  };

  private statsService: DashboardStats = new DashboardStats(store);

  async aboutToAppear(): Promise<void> {
    await this.loadStats();
  }

  async loadStats(): Promise<void> {
    try {
      this.stats = await this.statsService.getAllStats();
    } catch (error) {
      console.error('Failed to load stats', error);
    }
  }
}

4.2 刷新策略

typescript 复制代码
// 数据变更后刷新统计
async onDataChanged(): Promise<void> {
  // 更新统计
  await this.loadStats();

  // 如果有待处理候选,显示红点提示
  if (this.stats.pendingCandidates > 0) {
    // 显示导入引导
  }
}

4.3 统计卡片组件

typescript 复制代码
@Component
struct StatCard {
  @Prop label: string = '';
  @Prop count: number = 0;
  @Prop color: ResourceColor = '#4F7CFF';

  build() {
    Column() {
      Text(this.count.toString())
        .fontSize(28)
        .fontWeight(700)
        .fontColor(this.color)

      Text(this.label)
        .fontSize(13)
        .fontColor('#68708A')
        .margin({ top: 4 })
    }
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .layoutWeight(1)
  }
}

五、数据一致性保障

5.1 计数缓存

频繁切换页面时,每次都重新 COUNT 会影响性能。简单的内存缓存策略:

typescript 复制代码
export class StatsCache {
  private cache: {
    data: DashboardStats | null;
    timestamp: number;
  } = { data: null, timestamp: 0 };

  private readonly TTL = 5000; // 5 秒缓存

  async getStats(service: DashboardStats): Promise<DashboardStats> {
    const now = Date.now();

    if (this.cache.data && (now - this.cache.timestamp) < this.TTL) {
      return this.cache.data;
    }

    this.cache.data = await service.getAllStats();
    this.cache.timestamp = now;
    return this.cache.data;
  }

  invalidate(): void {
    this.cache.data = null;
    this.cache.timestamp = 0;
  }
}

5.2 写操作后失效

typescript 复制代码
// 在需要的地方注入缓存的失效逻辑
async onAddBinding(): Promise<void> {
  await this.bindingDao.insert(newBinding);
  statsCache.invalidate();  // 使缓存失效
  await this.loadStats();   // 重新加载
}

六、聚合查询的进阶

6.1 最大值/最小值

typescript 复制代码
// 查询最新的绑定更新时间
async getLatestUpdateTime(): Promise<string | null> {
  let predicates = new relationalStore.RdbPredicates('app_binding');
  predicates.orderByDesc('updated_at');
  predicates.limitAs(1);

  let rs = await this.store.query(predicates, ['updated_at']);
  try {
    if (rs.rowCount === 0) return null;
    rs.goToFirstRow();
    return rs.getString(0);
  } finally {
    rs.close();
  }
}

6.2 使用列投影模拟更多聚合

typescript 复制代码
// 统计每张卡的绑定数量
async countByCard(): Promise<Map<number, number>> {
  const all = await this.listAll();
  const map = new Map<number, number>();

  for (const item of all) {
    map.set(item.card_id, (map.get(item.card_id) || 0) + 1);
  }

  return map;
}

七、性能对比

方法 查询次数 数据传输 适用场景
逐状态 COUNT 4 次 4 个整数 数据量大,只需计数
全量查 + 内存统计 1 次 所有行数据 数据量小 (<1000)
Promise.all 并行 COUNT 4 次(并行) 4 个整数 推荐方案,兼顾性能

7.1 统计指标说明

指标 含义 数据来源 对应 UI
totalCards 卡号总数 card 表 首页卡号卡
totalBindings 绑定总数 app_binding 表 首页全部卡
activeBindings 使用中 app_binding.status='使用中' 使用中卡
pendingBindings 待处理 status='待换绑' + '待注销' 待处理卡
disabledBindings 已停用 app_binding.status='已停用' 已停用卡
pendingCandidates 待导入候选 sms_candidate.imported=0 导入提示

小结

本篇完成了数据统计的完整实现:

技能 方法 关键点
基础 COUNT count() 列投影 COUNT(*)
条件 COUNT countByStatus equalTo + COUNT
状态分布 countByStatusGroup 循环查询各状态
跨表统计 DashboardStats 组合多个 DAO
并行查询 Promise.all 4 个查询并行
内存缓存 StatsCache 5 秒 TTL 避免重复查询
缓存失效 invalidate() 写操作后刷新
聚合查询 MAX/MIN 查询 列投影 + 排序 + limit

7.2 状态统计维度

状态 含义 统计方法 首页显示
使用中 正常绑定的应用 countByStatus('使用中') 绿色标记
待换绑 标记为待换绑 countByStatus('待换绑') 黄色标记
待注销 标记为待注销 countByStatus('待注销') 橙色标记
已停用 已注销/停用 countByStatus('已停用') 灰色标记

下一篇将专题讲解「数据导出与备份 JSON 序列化」,实现数据库全量数据的 JSON 导出功能。


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


相关资源:

相关推荐
Leo.yuan1 小时前
数据目录和数据字典有什么区别?一文讲清
大数据·数据库·人工智能
拥抱太阳06161 小时前
HarmonyOS 应用开发《掌上英语》第15篇:泛型编程在 ArkTS 中的应用:BreakpointType 源码解读
华为·harmonyos
惜分飞1 小时前
记录block 0损坏,数据文件大量坏块,使用不当数据库版本恢复等各种操作之后的故障处理---惜分飞
数据库·block0损坏·ora-17503
执笔论英雄2 小时前
【大模型推理】redis kv cache
数据库·redis·缓存
绝世番茄2 小时前
鸿蒙原生 ArkTS 布局方式之 Swiper + itemSpace 卡片间距布局实战(API 24)
华为·harmonyos·鸿蒙
tiancaijiben2 小时前
阿里云SAE应用引擎弹性扩缩容完全指南:从手动调控到智能自动化
java·网络·数据库
ldsweet2 小时前
《HarmonyOS技术精讲-Basic Services Kit(基础服务)》开篇:全景概览与核心价值
华为·harmonyos
倔强的石头_2 小时前
8000 条 SQL 里,180 条外连接语义跑偏——Oracle 迁 KES 那些藏得最深的坑
数据库
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证
android·服务器·flutter·华为·harmonyos