31 数据更新与级联操作:update、delete 与数据一致性

前言

在这里插入图片描述

图:31 数据更新与级联操作:update、delete 与数据一致性 运行效果截图(HarmonyOS NEXT)

在数据库操作中,数据更新删除是比插入更敏感的操作——一旦出错可能造成数据丢失。对于有外键关联的表,还需要考虑级联操作:删除主表记录时,关联的子表记录应该如何处理?

本文以"鹿鹿·笔迹心理分析"项目中 ArchiveDao 的级联删除、HandwritingDao 的批量更新为例,深入解析鸿蒙 RDB 中的 update、delete 方法使用和数据一致性维护策略。

鸿蒙官方·RdbStore 更新删除:developer.huawei.com
项目源码仓库:harmony-app GitHub

数据更新与级联操作流程

图:级联删除流程——删除 archive 时自动清理关联的 handwriting 和 report

任一步骤异常

删除 archive 请求

ArchiveDao.delete

开始事务

删除关联 report
DELETE FROM report WHERE handwriting_id IN ...

删除关联 handwriting
DELETE FROM handwriting WHERE archive_id = ?

删除关联 relation
DELETE FROM relation WHERE archive_id = ?

删除 archive
DELETE FROM archive WHERE id = ?

提交事务

级联删除完成

回滚事务

一、update 更新操作

1.1 基本语法

await store.update(values: ValuesBucket, predicates: RdbPredicates): Promise<number>
  • values:要更新的字段和值
  • predicates:WHERE 条件
  • 返回值:受影响的行数

1.2 单字段更新

// UserDao.updateName — 更新用户名
static async updateName(id: number, name: string): Promise<void> {
  const store = UserDao.getStore()
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('id', id)
  await store.update({ name } as relationalStore.ValuesBucket, predicates)
}

1.3 多字段更新

// RelationDao.updateMatchScore — 更新匹配度
static async updateMatchScore(id: number, score: number): Promise<void> {
  const store = RelationDao.getStore()
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('id', id)
  await store.update({ match_score: score } as relationalStore.ValuesBucket, predicates)
}

1.4 原始 SQL 更新(绕过 Predicates)

// ArchiveDao.incrementRecord — 使用 executeSql 执行复杂更新
static async incrementRecord(archiveId: number, recordTime: number): Promise<void> {
  const store = ArchiveDao.getStore()
  await store.executeSql(
    'UPDATE archive SET record_count = record_count + 1, last_record_at = ? WHERE id = ?',
    [recordTime, archiveId]
  )
}

为什么要用 executeSql?

store.update() 只能传入固定的 ValuesBucket,不支持 record_count = record_count + 1 这种字段自增操作。executeSql 可以执行任意 SQL 语句。

操作 方法 支持的更新类型
固定值更新 store.update(values, predicates) SET column = ?
字段自增 store.executeSql(sql, args) SET column = column + 1
条件计算 store.executeSql(sql, args) SET column = fn(column)

二、delete 删除操作

2.1 基本语法

await store.delete(predicates: RdbPredicates): Promise<number>

2.2 单表删除

// RelationDao.delete — 删除关系绑定
static async delete(id: number): Promise<void> {
  const store = RelationDao.getStore()
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('id', id)
  await store.delete(predicates)
}

三、级联删除

3.1 ArchiveDao 的级联删除

鸿蒙 RDB 不支持外键约束的级联删除(CASCADE),需要手动在业务层实现:

// ArchiveDao.delete — 级联删除档案及关联数据
static async delete(id: number): Promise<void> {
  const store = ArchiveDao.getStore()

  // ① 删除档案本身
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('id', id)
  await store.delete(predicates)

  // ② 级联清理:handwriting 表中的关联记录 → archive_id 置为 0(未归类)
  const hwPredicates = new relationalStore.RdbPredicates('handwriting')
  hwPredicates.equalTo('archive_id', id)
  await store.update(
    { archive_id: 0 } as relationalStore.ValuesBucket,
    hwPredicates
  )

  // ③ 级联清理:report 表中的关联记录 → archive_id 置为 0
  const rptPredicates = new relationalStore.RdbPredicates('report')
  rptPredicates.equalTo('archive_id', id)
  await store.update(
    { archive_id: 0 } as relationalStore.ValuesBucket,
    rptPredicates
  )
}

3.2 级联策略比较

策略 做法 本项目选择 理由
置空(SET NULL) 关联字段设为 NULL 或 0 ✅ 设置为 archive_id = 0 笔迹数据有价值,不应删除
级联删除(CASCADE) 一起删除 丢失笔迹数据
拒绝(RESTRICT) 有子记录时不让删 用户应该能删除档案
忽略 不做任何处理 残留脏数据

设计原则:删除档案不删除笔迹和报告,而是将它们标记为"未归类"。因为笔迹和分析结果是用户创建的宝贵内容,删除档案只是改变了它们的组织方式。

3.3 事务保护下的级联操作

为了保证级联操作的原子性,应该使用事务包裹:

// 改进版:事务保护的级联删除
static async delete(id: number): Promise<void> {
  const store = ArchiveDao.getStore()
  try {
    await store.executeSql('BEGIN TRANSACTION')

    // 删除档案
    const predicates = new relationalStore.RdbPredicates(TABLE)
    predicates.equalTo('id', id)
    await store.delete(predicates)

    // 清理 handwriting
    const hwPredicates = new relationalStore.RdbPredicates('handwriting')
    hwPredicates.equalTo('archive_id', id)
    await store.update({ archive_id: 0 }, hwPredicates)

    // 清理 report
    const rptPredicates = new relationalStore.RdbPredicates('report')
    rptPredicates.equalTo('archive_id', id)
    await store.update({ archive_id: 0 }, rptPredicates)

    await store.executeSql('COMMIT')
  } catch (e) {
    await store.executeSql('ROLLBACK')    // 部分失败时全部回滚
    throw e
  }
}

四、批量更新

4.1 循环更新

// HandwritingDao.batchUpdateArchive — 将多条笔迹移入指定档案
static async batchUpdateArchive(ids: number[], archiveId: number): Promise<void> {
  const store = HandwritingDao.getStore()
  for (const id of ids) {
    const predicates = new relationalStore.RdbPredicates(TABLE)
    predicates.equalTo('id', id)
    await store.update(
      { archive_id: archiveId } as relationalStore.ValuesBucket,
      predicates
    )
  }
}

4.2 批量更新的优化

// 优化版:使用 SQL IN 一次性更新
static async batchUpdateArchive(ids: number[], archiveId: number): Promise<void> {
  const store = HandwritingDao.getStore()
  const placeholders: string = ids.map(() => '?').join(',')
  await store.executeSql(
    `UPDATE ${TABLE} SET archive_id = ? WHERE id IN (${placeholders})`,
    [archiveId, ...ids]
  )
}

性能对比:

方式 SQL 执行次数 适合的场景
循环 update(当前方式) N 次(每条一次) 少量(<10 条)
SQL IN 批量更新 1 次 批量(10-100 条)
事务包裹的循环 1 组事务 需要原子性

五、ArchiveDao.incrementRecord 模式

5.1 原子自增

static async incrementRecord(archiveId: number, recordTime: number): Promise<void> {
  const store = ArchiveDao.getStore()
  await store.executeSql(
    'UPDATE archive SET record_count = record_count + 1, last_record_at = ? WHERE id = ?',
    [recordTime, archiveId]
  )
}

为什么用 executeSql 而非 update:

// ❌ 无法用 update 实现
// update 的 values 是固定值
await store.update(
  { record_count: ???, last_record_at: timestamp } as ValuesBucket,
  predicates
)
// 问题:我们不知道当前的 record_count 值

// ✅ executeSql 直接操作数据库值
'record_count = record_count + 1'
// 数据库层面自增,不需要先查后改

5.2 批量增加

static async incrementRecordBy(
  archiveId: number, recordTime: number, count: number
): Promise<void> {
  const store = ArchiveDao.getStore()
  await store.executeSql(
    'UPDATE archive SET record_count = record_count + ?, last_record_at = ? WHERE id = ?',
    [count, recordTime, archiveId]
  )
}

六、更新/删除操作的数据一致性

6.1 项目中维护一致性的方法

一致性场景 做法 代码位置
删除档案→笔迹标记未归类 手动级联清理 ArchiveDao.delete
写入笔迹→档案计数 incrementRecord DemoSeeder
删除关系→档案不变 不级联 RelationDao.delete
更新用户名→档案不更新 不做级联(合理) UserDao.updateName

6.2 一致性策略总结

// 策略 1:级联清理(删除档案时)
ArchiveDao.delete(archiveId)
  → handwriting.archive_id = 0
  → report.archive_id = 0

// 策略 2:同步更新(写入笔迹时)
HandwritingDao.create(handwriting)
  → ArchiveDao.incrementRecord(archiveId, timestamp)
  // 手动增加档案的记录数

// 策略 3:不关联(关系不影响业务数据)
RelationDao.delete(relationId)
  → 不级联任何其他表
  // 解除关系不影响笔迹和报告

七、安全的删除检查

7.1 删前确认

在 UI 层,删除操作前应让用户确认:

// 弹窗确认删除
AlertDialog.show({
  title: '删除档案',
  message: '删除后笔迹将变为"未归类",确定要删除吗?',
  primaryButton: {
    value: '取消',
    action: () => {}
  },
  secondaryButton: {
    value: '删除',
    fontColor: '#E8919A',
    action: async () => {
      await ArchiveDao.delete(archiveId)
      // 刷新列表
    }
  }
})

7.2 异常回滚

try {
  await ArchiveDao.delete(id)
  // 更新 UI
} catch (e) {
  hilog.error(0x0000, 'ArchivePage', '删除失败: %s', JSON.stringify(e))
  // 提示用户操作失败
}

总结

本文深入解析了鸿蒙 RDB 中的更新和删除操作:

  1. store.update(values, predicates):固定值更新,适用于单/多字段修改
  2. store.delete(predicates):条件删除,返回受影响行数
  3. 级联删除实现:鸿蒙不支持 CASCADE 外键约束,需要手动实现级联操作
  4. 原子自增store.executeSql('SET col = col + 1') 实现字段自增
  5. 事务保护:多个写操作用 BEGIN TRANSACTION + COMMIT/ROLLBACK 包裹
  6. 数据一致性:删除档案时笔迹标记为未归类而非删除,保留用户数据

下一篇文章将总结 DAO 设计模式实战——项目中完整的 DAO 层设计。

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


参考资源:

  • RdbStore.update 方法
  • RdbStore.delete 方法
  • [ArchiveDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ArchiveDao.ets)
  • [HandwritingDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/HandwritingDao.ets)
  • [RelationDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/RelationDao.ets)
  • [UserDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/UserDao.ets)
  • SQLite 事务管理
  • 鸿蒙数据持久化指南
  • HarmonyOS 开发文档

七、软删除 vs 物理删除

本项目使用物理删除(直接从数据库中移除记录)。根据业务需求,有时软删除更合适:

策略 实现方式 优点 缺点
物理删除 DELETE FROM table WHERE id = ? 简单,数据库无冗余 无法恢复,级联复杂
软删除 UPDATE SET is_deleted = 1 WHERE id = ? 可恢复,保留历史 查询时需过滤 is_deleted

7.1 软删除实现示例

// 软删除:标记为已删除,不从数据库移除
async softDelete(id: string): Promise<void> {
  const store = DatabaseService.getInstance().getStore()
  const pred = new relationalStore.RdbPredicates('archive')
  pred.equalTo('id', id)

  const values: relationalStore.ValuesBucket = {}
  values['is_deleted'] = 1
  values['deleted_at'] = new Date().toISOString()
  await store.update(values, pred)
}

// 查询时自动过滤软删除记录
async findAll(userId: string): Promise<Archive[]> {
  const pred = new relationalStore.RdbPredicates('archive')
  pred.equalTo('user_id', userId)
      .equalTo('is_deleted', 0)  // 只查未删除的
      .orderByDesc('created_at')
  // ...
}

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐