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

图:31 数据更新与级联操作:update、delete 与数据一致性 运行效果截图(HarmonyOS NEXT)
在数据库操作中,数据更新和删除是比插入更敏感的操作——一旦出错可能造成数据丢失。对于有外键关联的表,还需要考虑级联操作:删除主表记录时,关联的子表记录应该如何处理?
本文以"鹿鹿·笔迹心理分析"项目中 ArchiveDao 的级联删除、HandwritingDao 的批量更新为例,深入解析鸿蒙 RDB 中的 update、delete 方法使用和数据一致性维护策略。
鸿蒙官方·RdbStore 更新删除:developer.huawei.com
项目源码仓库:harmony-app GitHub

图:级联删除流程——删除 archive 时自动清理关联的 handwriting 和 report
一、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 中的更新和删除操作:
- store.update(values, predicates):固定值更新,适用于单/多字段修改
- store.delete(predicates):条件删除,返回受影响行数
- 级联删除实现:鸿蒙不支持 CASCADE 外键约束,需要手动实现级联操作
- 原子自增:
store.executeSql('SET col = col + 1')实现字段自增 - 事务保护:多个写操作用
BEGIN TRANSACTION+COMMIT/ROLLBACK包裹 - 数据一致性:删除档案时笔迹标记为未归类而非删除,保留用户数据
下一篇文章将总结 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')
// ...
}
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)