鸿蒙应用开发实战【34】— 批量操作:batchUpdateStatus 与 deleteMany

鸿蒙应用开发实战【34】--- 批量操作:batchUpdateStatus 与 deleteMany

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

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


前言

在号码助手的首页,用户可以多选 应用绑定记录,然后执行批量更新状态批量删除 。这些操作在后端 DAO 层对应两个关键方法:batchUpdateStatusdeleteMany。本篇将深入 RDB 的批量操作机制,涵盖 predicates.in() 的完整用法、事务保护、与 UI 多选模式的数据联动。

本篇涵盖predicates.in() 实现批量过滤、batchUpdateStatus 完整实现、deleteMany 完整实现、事务保护的批量操作、操作前后的数据校验、批量操作返回值的含义解读、避免全表更新的防御编程。


一、predicates.in() 批量过滤

批量操作的核心是 predicates.in(field, values)------它生成 WHERE field IN (val1, val2, ...) 的 SQL 条件。

typescript 复制代码
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.in('id', [1, 5, 8, 12, 20]);
// 生成 WHERE id IN (1, 5, 8, 12, 20)

这是 RDB 执行批量操作的标准入口。in 方法支持 number[]string[] 两种类型。

⚠️ 空数组 :如果传入空数组,RDB 生成 WHERE id IN () --- 这在 SQLite 中是合法的空集合,匹配 0 行。建议在 DAO 层前置检查空数组,提前返回。


二、batchUpdateStatus

2.1 基本实现

typescript 复制代码
async batchUpdateStatus(ids: number[], newStatus: string): Promise<number> {
  if (ids.length === 0) return 0;

  let predicates = new relationalStore.RdbPredicates('app_binding');
  predicates.in('id', ids);

  const values: relationalStore.ValuesBucket = {
    'status': newStatus,
    'updated_at': new Date().toISOString(),
  };

  const changedRows = await this.store.update(values, predicates);
  console.info(`batchUpdateStatus: ids=${ids.length}, status=${newStatus}, affected=${changedRows}`);
  return changedRows;
}

2.2 返回值解读

update 返回的是受影响的行数 。以下情况返回值可能不等于 ids.length

  • 某些 id 不存在---实际更新的行数 < ids.length
  • 状态已经是目标值---RDB 仍会计入受影响行数
  • 并发修改---可能有外部因素影响

不要在调用方假设返回值等于 ids.length,它只是参考值。

2.3 带校验的批量更新

typescript 复制代码
/**
 * 批量更新状态(带前置校验)
 * 只更新那些状态合法的记录
 */
async safeBatchUpdateStatus(ids: number[], newStatus: string): Promise<{
  updated: number;
  skipped: number;
  errors: string[];
}> {
  if (ids.length === 0) return { updated: 0, skipped: 0, errors: [] };

  let updated = 0;
  let skipped = 0;
  const errors: string[] = [];

  try {
    this.store.beginTransaction();

    for (const id of ids) {
      const binding = await this.getById(id);
      if (!binding) {
        errors.push(`Binding ${id} not found`);
        skipped++;
        continue;
      }

      // 状态转换合法性检查
      if (!isValidTransition(binding.status, newStatus)) {
        errors.push(`Invalid transition: ${binding.status} → ${newStatus}`);
        skipped++;
        continue;
      }

      await this.updateStatus(id, newStatus);
      updated++;
    }

    this.store.commit();
  } catch (error) {
    this.store.rollback();
    throw error;
  }

  return { updated, skipped, errors };
}

/**
 * 状态转换合法性验证
 */
function isValidTransition(current: string, target: string): boolean {
  const validTransitions: Record<string, string[]> = {
    '使用中': ['待换绑', '待注销'],
    '待换绑': ['使用中', '已停用'],
    '待注销': ['已停用'],
    '已停用': [], // 终态,不可再转换
  };

  return validTransitions[current]?.includes(target) ?? false;
}

2.4 批量更新方案对比

方案 安全校验 事务 性能 适用场景
batchUpdateStatus 最快 信任调用方
safeBatchUpdateStatus ✅ 状态转换检查 ✅ 是 稍慢 UI 批量操作
分批限制版 分批防超长 ✅ 是 超大批量 (>500)

三、deleteMany

3.1 基本实现

typescript 复制代码
async deleteMany(ids: number[]): Promise<number> {
  if (ids.length === 0) return 0;

  let predicates = new relationalStore.RdbPredicates('app_binding');
  predicates.in('id', ids);

  const deletedRows = await this.store.delete(predicates);
  console.info(`deleteMany: ids=${ids.length}, affected=${deletedRows}`);
  return deletedRows;
}

3.2 外键关联的安全删除

删除 app_binding 时如果有关联的 card 数据,需先检查:

typescript 复制代码
/**
 * 安全批量删除:先检查每条绑定是否存在关联引用
 * 返回实际删除数量和跳过的 ID
 */
async safeDeleteMany(ids: number[]): Promise<{
  deleted: number;
  skipped: { id: number; reason: string }[];
}> {
  const skipped: { id: number; reason: string }[] = [];
  const toDelete: number[] = [];

  // 逐条检查
  for (const id of ids) {
    const binding = await this.getById(id);
    if (!binding) {
      skipped.push({ id, reason: 'not_found' });
      continue;
    }
    if (binding.status === '使用中') {
      skipped.push({ id, reason: 'active_binding' });
      continue;
    }
    toDelete.push(id);
  }

  // 批量删除合法的记录
  let deleted = 0;
  if (toDelete.length > 0) {
    deleted = await this.deleteMany(toDelete);
  }

  return { deleted, skipped };
}

四、事务保护的批量操作组合

前端多选操作通常包含多个步骤------例如"批量换绑":先批量标记状态,再记录操作日志。需要用事务保证原子性:

typescript 复制代码
/**
 * 批量换绑操作(事务保护)
 */
async batchSwitch(switchItems: {
  bindingId: number;
  newCardId: number;
}[]): Promise<void> {
  try {
    this.store.beginTransaction();

    for (const item of switchItems) {
      // 1. 获取当前绑定
      const binding = await this.getById(item.bindingId);
      if (!binding) {
        console.warn(`Binding ${item.bindingId} not found, skipping`);
        continue;
      }

      // 2. 旧绑定标记为待换绑
      await this.updateStatus(item.bindingId, '待换绑');

      // 3. 创建新绑定
      await this.insert({
        app_name: binding.app_name,
        card_id: item.newCardId,
        category: binding.category,
        status: '使用中',
      });
    }

    this.store.commit();
    console.info(`batchSwitch completed: ${switchItems.length} items`);
  } catch (error) {
    this.store.rollback();
    throw error;
  }
}

五、跨表的批量操作

有时需要跨 card 和 app_binding 两张表操作。例如批量删除卡号时清理关联绑定:

typescript 复制代码
import { CardDao } from './CardDao';
import { AppBindingDao } from './AppBindingDao';

export class BatchOperationService {
  private store: relationalStore.RdbStore;
  private cardDao: CardDao;
  private bindingDao: AppBindingDao;

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

  /**
   * 批量删除卡号 + 清理关联绑定
   */
  async deleteCardsWithBindings(cardIds: number[]): Promise<{
    deletedCards: number;
    deletedBindings: number;
  }> {
    try {
      this.store.beginTransaction();

      // 1. 清理关联绑定(先清理绑定,再删除卡号)
      for (const cardId of cardIds) {
        await this.bindingDao.deleteByCardId(cardId);
      }

      // 2. 删除卡号
      const deletedCards = await this.cardDao.deleteMany(cardIds);
      const deletedBindings = cardIds.length; // 近似值

      this.store.commit();
      return { deletedCards, deletedBindings };
    } catch (error) {
      this.store.rollback();
      throw error;
    }
  }
}

📌 删除顺序 :先删绑定再删卡号------如果先删卡号,外键约束会导致绑定记录的 card_id 指向不存在的卡。


六、与 UI 多选模式的联动

6.1 典型流程

复制代码
UI:用户勾选 N 条记录 → 点击"删除"按钮
     ↓
UI:确认对话框 → 用户确认
     ↓
UI:调用 DAO.deleteMany(selectedIds)
     ↓
DAO:predicates.in('id', ids) → RdbStore.delete()
     ↓
DAO:返回 affectedRows
     ↓
UI:根据返回值刷新列表 → 显示 Toast 提示

6.2 UI 层代码示例

typescript 复制代码
@Entry
@Component
struct HomePage {
  @State selectedIds: number[] = [];
  private bindingDao: AppBindingDao = new AppBindingDao(store);

  async onBatchDelete(): Promise<void> {
    if (this.selectedIds.length === 0) return;

    try {
      const result = await this.bindingDao.safeDeleteMany(this.selectedIds);

      if (result.skipped.length > 0) {
        // 有跳过的记录,提示用户
        console.warn(`Skipped ${result.skipped.length} items:`,
          result.skipped.map(s => `id=${s.id}: ${s.reason}`).join(', '));
      }

      // 刷新列表
      this.loadData();
      this.selectedIds = [];

      // 显示提示
      AlertDialog.show({
        message: `成功删除 ${result.deleted} 条记录`
          + (result.skipped.length > 0 ? `,${result.skipped.length} 条跳过` : ''),
      });
    } catch (error) {
      AlertDialog.show({ message: `删除失败:${error.message}` });
    }
  }
}

七、防御编程与常见坑

7.1 空数组防护

typescript 复制代码
// 每次批量操作前检查
if (ids.length === 0) return 0;
// 或
if (ids.length === 0) return;

7.2 最大批量限制

RDB 单条 SQL 的 IN 子句没有硬性限制,但过多的 ID 可能导致 SQL 长度超出限制:

typescript 复制代码
const MAX_BATCH_SIZE = 500;

async batchUpdateStatusSafe(ids: number[], newStatus: string): Promise<number> {
  let totalAffected = 0;

  // 分批处理
  for (let i = 0; i < ids.length; i += MAX_BATCH_SIZE) {
    const batch = ids.slice(i, i + MAX_BATCH_SIZE);
    const affected = await this.batchUpdateStatus(batch, newStatus);
    totalAffected += affected;
  }

  return totalAffected;
}

7.3 不要忘记更新时间

批量更新状态时,记得更新 updated_at 时间戳:

typescript 复制代码
// ✅ 正确:更新 updated_at
await this.store.update({
  'status': newStatus,
  'updated_at': new Date().toISOString(),
}, predicates);

// ❌ 错误:time 字段不会被自动更新
await this.store.update({ 'status': newStatus }, predicates);

小结

本篇深入讲解了批量操作的完整实现:

操作 方法 关键点
批量过滤 predicates.in() IN 子句生成
批量更新状态 batchUpdateStatus 更新 affected 行数
带校验批量更新 safeBatchUpdateStatus 状态转换合法性检查
批量删除 deleteMany predicates.in + delete
安全批量删除 safeDeleteMany 前置检查活跃记录
批量换绑 batchSwitch 跨表事务操作
批量删卡+清理 deleteCardsWithBindings 跨表 + 删除顺序
最大批量限制 MAX_BATCH_SIZE = 500 分批处理大数组

7.4 批量操作方法汇总

方法 操作类型 事务保护 安全校验 返回值
batchUpdateStatus(ids, status) 批量更新 number 影响行数
safeBatchUpdateStatus(ids, status) 批量更新 ✅ 状态转换 {updated,skipped,errors}
deleteMany(ids) 批量删除 number 影响行数
safeDeleteMany(ids) 批量删除 ✅ 活跃检查 {deleted,skipped}
batchSwitch(items) 批量换绑 void
deleteCardsWithBindings(cardIds) 跨表删除 {deletedCards,deletedBindings}

下一篇将专题讲解「DAO 层错误处理规范与单元测试」,涵盖 try-catch-finally 模式、错误类型分类、Hypium 单元测试框架在 DAO 层的实践。


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


相关资源:

相关推荐
贾伟康4 小时前
【安心陪诊 Agent v1.1】首页可信设计实战:HTML/CSS 状态卡与急症分流
css·html·harmonyos·ai agent·首页设计
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第16篇:Worker 多线程与异步编程 — ArkTS 并发方案在记账应用中的实践
harmonyos
全堆鸿蒙5 小时前
鸿蒙应用开发实战【37】— 数据统计:countByStatus 聚合查询
数据库·华为·harmonyos·鸿蒙系统
拥抱太阳06165 小时前
HarmonyOS 应用开发《掌上英语》第15篇:泛型编程在 ArkTS 中的应用:BreakpointType 源码解读
华为·harmonyos
绝世番茄5 小时前
鸿蒙原生 ArkTS 布局方式之 Swiper + itemSpace 卡片间距布局实战(API 24)
华为·harmonyos·鸿蒙
ldsweet5 小时前
《HarmonyOS技术精讲-Basic Services Kit(基础服务)》开篇:全景概览与核心价值
华为·harmonyos
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证
android·服务器·flutter·华为·harmonyos
FrameNotWork6 小时前
HarmonyOS 6.0 WebView嵌入与交互
华为·交互·harmonyos
b130538100496 小时前
鸿蒙应用开发实战【41】— 登录页面LoginPage完整开发
服务器·华为·harmonyos·鸿蒙系统