鸿蒙应用开发实战【34】--- 批量操作:batchUpdateStatus 与 deleteMany
本文是「号码助手全栈开发系列」第 34 篇,持续更新中...
前言
在号码助手的首页,用户可以多选 应用绑定记录,然后执行批量更新状态 或批量删除 。这些操作在后端 DAO 层对应两个关键方法:batchUpdateStatus 和 deleteMany。本篇将深入 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 层的实践。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 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