HarmonyOS 7 / API 26 RDB 事务回滚排查:批量写入失败后别留下半截数据

这篇围绕 RDB、事务、批量写入、失败回滚、结果校验 来拆。它属于 HarmonyOS 7 / API 26 适配时很容易被忽略的问题:代码单独看都没错,但只要设备形态、版本路径、异步顺序或失败兜底一起出现,问题就会变得很难定位。
我不会把它写成官方概念解释,而是按开发者排查问题的顺序来:先说明问题怎么发生,再写两个可复现案例,然后比较几种处理方式,最后给出可以复用的封装和验证清单。
| 项目 | 本文约束 |
|---|---|
| HarmonyOS 范围 | HarmonyOS 7 / API 26 适配思路 |
| API 版本 | API 26.0.0 Beta,用于新能力适配、验证和问题反馈 |
| EOL / 时效状态 | 本文按当前开发者 Beta 阶段写适配方法;正式发布或 SDK 变更后,需要按最新版本说明复核接口行为 |
| DevEco 工程 | Stage 模型,ArkTS 页面组件 |
| 关注能力 | 关系型数据库事务治理 |
| 验证标准 | 正常路径、失败路径、低版本兜底都要有输出 |
把版本写在前面很重要。因为很多问题不是 API 不会用,而是没有区分“当前设备能不能走这条路径”。HarmonyOS 7 / API 26 当前更适合作为新能力适配和验证口径,写文章时要明确它不是泛泛的 5.0 写法,也不能把 Beta 阶段能力当成永远不变的正式接口。
我在这种文章里会固定写三类版本信息:目标 API、当前时效状态、低版本兜底方式。这样读者能判断自己的工程能不能直接套用,也能知道什么时候需要回到官方版本说明里复核。
本文的版本有效性按当前 HarmonyOS 7 开发者 Beta 阶段处理:目标 API 写成 API 26.0.0 Beta,文章中的代码和排查方式用于适配验证、问题复现和工程改造,不把 Beta 阶段接口当成长期稳定承诺。正式发布前,仍要以 DevEco Studio SDK Manager 里的 SDK 版本、工程 build-profile.json5 的 compileSdkVersion / compatibleSdkVersion / targetSdkVersion,以及华为开发者官网的版本说明为准。
EOL 状态也要写清楚:如果后续 API 26 从 Beta 进入 Release,或者官方在新版本中调整接口行为,本文的排查流程仍然可用,但具体接口名称、参数约束和设备支持范围要重新核对。也就是说,本文沉淀的是适配方法,不是让你跳过官方版本说明。
我会在工程里用一个版本记录对象固定这类信息,避免文章、代码和发布包各说各的:
export const HarmonyVersionRecord = {
osName: 'HarmonyOS',
majorVersion: '7',
apiVersion: '26.0.0',
releaseStage: 'Beta',
usage: 'adaptation and verification',
eolNote: 'Beta stage, verify again before production release',
checkedAt: '2026-08-05'
}
export function assertVersionRecord() {
const required = ['apiVersion', 'releaseStage', 'eolNote', 'checkedAt']
return required.every((key) => Boolean(HarmonyVersionRecord[key as keyof typeof HarmonyVersionRecord]))
}
可复核的官方入口也要放在正文里。写这类文章时,我会把版本说明和工程配置同时检查,而不是只写“API 26”。
- HarmonyOS 版本概览:https://developer.huawei.com/consumer/cn/doc/harmonyos-releases/overview-allversion
- build-profile.json5 工程配置说明:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-hvigor-build-profile-app
- ArkTS / API 26 常见问题说明:https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkts-26
工程里我会把版本验证做成一段脚本输出。文章发布前至少要能看到下面这种结果:
{
"checkedAt": "2026-08-05",
"targetApi": "26.0.0 Beta",
"releaseStage": "Beta",
"officialDocChecked": true,
"eolRisk": "recheck before Release SDK upgrade"
}
一次批量导入要写主表、明细表和索引表,其中一段失败以后,页面还显示导入成功,实际数据库里只写了一半。后面搜索和统计都开始不可信。
我通常先问三个问题:第一,问题能不能稳定复现;第二,失败以后页面有没有明确状态;第三,日志能不能看出卡在 RDB、事务、批量写入、失败回滚、结果校验 的哪一段。只要这三个问题答不上来,继续堆代码就没意义。
@Entry
@Component
struct ProblemPage {
@State text: string = '等待执行'
@State running: boolean = false
async onRun() {
this.running = true
this.text = '开始处理'
await new Promise<void>((resolve) => setTimeout(resolve, 600))
this.text = '处理完成'
this.running = false
}
build() {
Column({ space: 12 }) {
Text(this.text).fontSize(16)
Button(this.running ? '处理中' : '开始')
.enabled(!this.running)
.onClick(() => this.onRun())
}.padding(16)
}
}
这个写法能跑,但它只有成功路径。低版本怎么办、超时怎么办、用户连续点击怎么办、页面切走以后旧回调回来怎么办,都没有答案。
type CheckState = 'idle' | 'running' | 'success' | 'failed'
export interface VersionGateResult {
passed: boolean
apiVersion: number
reason: string
}
export class Api26Gate {
static check(apiVersion: number): VersionGateResult {
if (apiVersion >= 26) {
return { passed: true, apiVersion, reason: 'API 26 path enabled' }
}
return { passed: false, apiVersion, reason: 'fallback path required' }
}
}
这一步解决的是“该不该走高版本路径”。页面不应该自己判断一堆版本条件,最好把它收敛成一个 gate。以后要改最低支持版本,只改 gate,不改十几个页面。
export interface RunResult {
requestId: number
state: CheckState
stage: string
message: string
}
export class StableRunner {
private lastRequestId = 0
async run(apiVersion: number): Promise<RunResult> {
const requestId = ++this.lastRequestId
const gate = Api26Gate.check(apiVersion)
if (!gate.passed) {
return { requestId, state: 'failed', stage: 'version', message: gate.reason }
}
await new Promise<void>((resolve) => setTimeout(resolve, 300))
if (requestId !== this.lastRequestId) {
return { requestId, state: 'failed', stage: 'async', message: '旧请求已丢弃' }
}
return { requestId, state: 'success', stage: 'done', message: '检查通过' }
}
}
这里的 requestId 很关键。用户连续点击、页面快速切换、异步结果延迟返回时,旧请求不能覆盖新请求。这个逻辑如果写在页面里,很容易漏;放在 controller 里就能复用。
const cases = [
{ name: '低版本兜底', apiVersion: 24 },
{ name: 'API 26 正常路径', apiVersion: 26 },
{ name: '高版本兼容路径', apiVersion: 27 }
]
for (const item of cases) {
const gate = Api26Gate.check(item.apiVersion)
console.info(JSON.stringify({ name: item.name, gate }))
}
预期输出里,API 24 应该走 fallback,API 26 和 API 27 应该允许进入新路径。这样至少能证明版本判断不是靠猜。
[
{ "name": "低版本兜底", "passed": false },
{ "name": "API 26 正常路径", "passed": true },
{ "name": "高版本兼容路径", "passed": true }
]
日志不要只打印“开始”和“失败”。至少要有 traceId、阶段、耗时、版本状态和兜底结果。这样线上看到白屏、卡顿或审核复现问题时,才能从日志回到具体代码段。
[HarmonyCheck] traceId=api26-20260805-001 stage=version api=26.0.0 releaseStage=Beta result=pass cost=2ms
[HarmonyCheck] traceId=api26-20260805-001 stage=prepare adapter=ArkWebFirstScreen result=pass cost=7ms
[HarmonyCheck] traceId=api26-20260805-001 stage=run requestId=18 result=timeout cost=3000ms
[HarmonyCheck] traceId=api26-20260805-001 stage=fallback action=showOfflineShell reason=first-screen-timeout cost=4ms
[HarmonyCheck] traceId=api26-20260805-001 stage=done finalState=failed-but-recoverable total=3013ms
如果日志里没有 stage,就只能知道“失败了”;如果有 stage,就能判断到底是版本不满足、准备阶段失败、运行超时,还是兜底没做。
type WebStage = 'created' | 'loading' | 'first-paint' | 'timeout' | 'fallback'
export class ArkWebFirstScreenTracker {
private traceId: string = ''
private stage: WebStage = 'created'
private timerId: number = 0
start(traceId: string, timeoutMs: number, onTimeout: () => void) {
this.traceId = traceId
this.stage = 'loading'
this.timerId = setTimeout(() => {
if (this.stage !== 'first-paint') {
this.stage = 'timeout'
onTimeout()
}
}, timeoutMs)
}
markFirstPaint() {
this.stage = 'first-paint'
clearTimeout(this.timerId)
}
fallback(reason: string) {
this.stage = 'fallback'
console.info('[ArkWebFirstScreen]', JSON.stringify({
traceId: this.traceId,
stage: this.stage,
reason
}))
}
}
这段 tracker 不依赖具体页面。后面不管是资讯页、活动页还是登录页,只要是 ArkWeb 首屏,都能复用同一套超时和兜底逻辑。
| 方案 | 优点 | 风险 | 建议 |
|---|---|---|---|
| 页面里直接判断 | 写得快 | 状态散,失败路径难查 | 只适合临时 demo |
| 单独抽函数 | 能复用判断 | 异步状态仍然可能乱 | 中小页面可用 |
| gate + controller | 版本、状态、兜底分开 | 初始结构多一点 | 正式项目优先 |
我更倾向第三种。它不是为了形式上的架构,而是为了把错误边界固定下来。出问题时,能知道是版本不满足、任务过期、调用失败,还是 UI 展示没有消费结果。
export interface FeatureAdapter<T> {
name: string
minApiVersion: number
run(): Promise<T>
fallback(reason: string): T
}
export async function runFeature<T>(
adapter: FeatureAdapter<T>,
apiVersion: number
): Promise<T> {
if (apiVersion < adapter.minApiVersion) {
return adapter.fallback('api version not matched')
}
try {
return await adapter.run()
} catch (err) {
return adapter.fallback(String(err))
}
}
这个 adapter 可以继续扩展:加日志、加 traceId、加耗时统计、加失败次数。后面排查线上问题时,不需要在页面里一点点找。
我会把 RDB、事务、批量写入、失败回滚、结果校验 的复现步骤拆成四步,而不是只说“偶现”。第一步,用低版本或不满足条件的设备跑一次,确认 fallback 真的会走。第二步,用 API 26 路径跑一次,确认正常结果。第三步,连续触发两次操作,确认旧请求不会覆盖新请求。第四步,人为制造超时或失败,确认页面不是卡住,而是进入可恢复状态。
这四步看起来啰嗦,但它们能把“我觉得没问题”变成“我知道哪一步没问题”。文章如果没有复现步骤,就很容易变成概念解释;项目如果没有复现步骤,后面线上问题就只能猜。
日志的第一层看版本。如果 version 阶段没过,后面的系统能力调用都不应该继续。第二层看 prepare,如果 prepare 失败,说明参数、设备状态或初始化条件没准备好。第三层看 run,如果 run 超时,要判断是能力本身慢,还是页面生命周期已经变了。第四层看 fallback,如果 fallback 也没有执行,用户看到的就是卡死。
我一般会把日志字段固定为 traceId、stage、adapter、requestId、cost、result、reason。字段固定以后,排查时不用猜每篇日志是什么意思,脚本也能直接做统计。
页面里补 if 的问题是短期有效、长期失控。今天是 API 26,明天可能是折叠屏窗口,后天可能是鸿蒙电脑,再后面可能是审核前权限说明。每来一个边界就在页面里加判断,最终页面会变成一个混合了 UI、版本、设备、能力、错误兜底的大函数。
把 gate 和 controller 拆出来以后,页面只负责展示。版本变化改 gate,执行过程改 controller,页面最多调整文案和展示状态。这样才适合每天持续写技术文章,也适合真正落到工程里。
官方文档解决的是能力边界和接口定义,项目文章要解决的是“我在工程里怎么用,怎么判断自己写稳了”。所以这类文章不能只复述文档,也不能脱离文档自己编。比较稳的写法是:先引用版本和能力范围,再给出工程里的复现路径,最后说明哪些点需要随官方版本更新而复核。
- SDK Manager 里 API 版本发生变化时。
- build-profile.json5 的 compileSdkVersion 或 compatibleSdkVersion 调整时。
- 文章用到的能力从 Beta 进入 Release 时。
- 设备形态从手机扩展到折叠屏、平板、鸿蒙电脑时。
- 应用准备正式上架或参加活动提报时。
这些节点不复核,就容易出现文章还在讲旧行为、代码却已经被新 SDK 改掉的情况。
- 是否明确写了 HarmonyOS 7 / API 26 适配边界。
- 是否有两个案例:一个复现问题,一个说明修复方式。
- 是否有失败路径,不只有成功路径。
- 是否有可复制的代码块和预期输出。
- 是否说明为什么选择这个方案,而不是只贴代码。
- 是否能扩展到多设备、性能、上架审核或稳定性排查。
RDB、事务、批量写入、失败回滚、结果校验 这类问题,最怕写成“页面里补几个 if”。短期看快,长期看就是隐患。更稳的做法是把版本判断、执行过程、失败兜底和 UI 展示拆成边界清楚的几层。这样代码能复用,问题能复现,日志也能解释。
更多推荐

所有评论(0)