杨利杰YJlio博客头图

数据库记录列表展示与更新

大家好,我是杨利杰 YJlio。

上一节整理了工单表的基础读写。这一节继续使用TicketLearn.db和TICKET表,让查询结果进入列表,再编辑一条记录的标题,保存后重新查询。

本篇编号028,计划日期2026年10月18日。原稿保留。原稿的字符串数组只是布局数据,不是数据库查询结果。下面补充同一页面文件中配套使用的学习代码,尚未在本机编译或设备运行;图片均为概念示意。

一、从静态字符串转为业务记录

原稿中的网络无法连接、打印机驱动安装可用于看布局,但标题可能重复,不适合作为业务记录的唯一身份。本篇沿用上一节的三个字段:

interface Ticket {
  id: string;
  title: string;
  status: string;
}

数据库列ID、TITLE、STATUS分别映射到id、title、status。列表数据来自查询,不在页面初始化时伪造两条“数据库记录”。本篇也不自动插入或删除样例数据。

静态示例改为Ticket数组

二、查询完成后整体替换列表状态

以下函数与后面的页面放在同一个Index.ets学习文件中。已有工程可拆成模块,但不要重复声明同名函数。

import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function openStore(context: common.UIAbilityContext): Promise<relationalStore.RdbStore> {
  const config: relationalStore.StoreConfig = {
    name: 'TicketLearn.db',
    securityLevel: relationalStore.SecurityLevel.S3
  };
  const store = await relationalStore.getRdbStore(context, config);
  await store.executeSql('CREATE TABLE IF NOT EXISTS TICKET (' +
    'ID TEXT PRIMARY KEY NOT NULL, TITLE TEXT NOT NULL, STATUS TEXT NOT NULL)');
  return store;
}

async function loadTickets(store: relationalStore.RdbStore): Promise<Ticket[]> {
  const predicates = new relationalStore.RdbPredicates('TICKET');
  const result = await store.query(predicates, ['ID', 'TITLE', 'STATUS']);
  try {
    const rows: Ticket[] = [];
    const idIndex = result.getColumnIndex('ID');
    const titleIndex = result.getColumnIndex('TITLE');
    const statusIndex = result.getColumnIndex('STATUS');
    while (result.goToNextRow()) {
      rows.push({
        id: result.getString(idIndex),
        title: result.getString(titleIndex),
        status: result.getString(statusIndex)
      });
    }
    return rows;
  } finally {
    result.close();
  }
}

查询返回新数组后,再赋给页面的@State tickets。这里使用整体赋值,不直接修改数组内对象的深层属性。状态观察边界可参考官方@State文档。

表结构沿用027;IF NOT EXISTS不负责旧表迁移。首次打开的空库可能没有记录,这是正常的空表情形。

查询完成再替换列表状态

三、列表只展示查询到的字段

每行显示业务ID、标题和状态,编辑按钮只负责选择这一行的ID与标题。读取失败单独显示错误,不用空数组掩盖异常。

加载中、正常空表与加载失败要分开。已有列表刷新失败时保留旧内容,同时显示错误,让用户知道当前内容可能尚未更新。

列表展示三项信息

四、本例行键包含ID与展示内容

ForEach的键用来决定子项复用。本例没有在行内保留输入框状态,编辑框放在列表外,因此选择将id、title、status组合序列化作为行键:

(ticket: Ticket) => JSON.stringify([ticket.id, ticket.title, ticket.status])

未变化的记录保持相同键;显示内容变化后,该行使用新键重建。这是本例处理简单展示行的明确选择,不能当成所有列表的通用键策略。若行内有输入、焦点或复杂组件状态,应另做稳定业务ID与子组件状态传递设计。

业务身份仍是ID;本例的行键还包含展示内容,用于决定这一行何时重建。

不要只拿标题做唯一键。两条工单可以同名,编辑标题也不应改变它们的业务ID。更多规则见官方ForEach文档。

行键同时包含展示内容

五、编辑草稿与已加载列表分开

selectedId保存当前编辑的业务ID,draftTitle保存输入框中的标题草稿。点击编辑时复制当前标题;输入变化只修改草稿,不立即写库,也不直接改列表里的ticket.title。

这样可以区分“输入框已经修改”与“数据库已经保存”。本例保存前仍做必填和80长度限制,计数使用UTF-16码元的string.length,与024的限制口径一致。

编辑值与列表值分开

六、按ID保存标题,再重新查询

async function saveTitle(
  store: relationalStore.RdbStore,
  id: string,
  input: string
): Promise<number> {
  const title = input.trim();
  if (id.trim().length === 0 || title.length === 0 || title.length > 80) {
    throw new Error('请选择记录并填写1至80长度的标题');
  }
  const predicates = new relationalStore.RdbPredicates('TICKET');
  predicates.equalTo('ID', id);
  const values: relationalStore.ValuesBucket = { TITLE: title };
  return await store.update(values, predicates);
}

保存只修改匹配业务ID的TITLE,不改ID和STATUS。返回影响行数后,页面核对是否为1,再调用loadTickets读取当前记录。

本例没有把“输入框改了”当成“保存完成”,也没有用本地猜测的标题替代更新后的查询结果。

按ID更新TITLE后再查询

七、区分更新失败与更新后的刷新失败

页面使用busy串行处理本页面操作,按钮与输入框在处理期间禁用。它不是数据库全局锁,也没有解决其他页面或进程的并发写入。

@Entry
@Component
struct Index {
  @State tickets: Ticket[] = [];
  @State busy: boolean = false;
  @State message: string = '';
  @State selectedId: string = '';
  @State draftTitle: string = '';

  aboutToAppear(): void {
    this.refresh();
  }

  private async getStore(): Promise<relationalStore.RdbStore> {
    const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    if (!context) {
      throw new Error('无法取得页面宿主上下文');
    }
    return await openStore(context);
  }

  private async refresh(): Promise<void> {
    if (this.busy) return;
    this.busy = true;
    this.message = '';
    try {
      const store = await this.getStore();
      this.tickets = await loadTickets(store);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`refresh failed: ${err.code}, ${err.message}`);
      this.message = '加载失败,保留原列表';
    } finally {
      this.busy = false;
    }
  }

  private async save(): Promise<void> {
    if (this.busy) return;
    this.busy = true;
    this.message = '';
    let updated = false;
    try {
      const store = await this.getStore();
      const changed = await saveTitle(store, this.selectedId, this.draftTitle);
      if (changed !== 1) {
        throw new Error(`影响行数异常:${changed}`);
      }
      updated = true;
      this.tickets = await loadTickets(store);
      this.draftTitle = this.draftTitle.trim();
      this.message = '更新和刷新完成';
    } catch (error) {
      const err = error as BusinessError;
      console.error(`save failed: ${err.code}, ${err.message}`);
      this.message = updated ? '更新已完成,但刷新失败,请重新加载' : `更新失败:${err.message}`;
    } finally {
      this.busy = false;
    }
  }

  build() {
    Column({ space: 12 }) {
      Button('重新加载').enabled(!this.busy).onClick(() => {
        this.refresh()
      })
      Text(this.busy ? '处理中' : this.message)
      if (!this.busy && this.message.length === 0 && this.tickets.length === 0) {
        Text('暂无记录')
      }
      List() {
        ForEach(this.tickets, (ticket: Ticket) => {
          ListItem() {
            Column({ space: 6 }) {
              Text(`${ticket.id}|${ticket.title}|${ticket.status}`)
              Button('编辑标题').enabled(!this.busy).onClick(() => {
                this.selectedId = ticket.id;
                this.draftTitle = ticket.title;
              })
            }.padding(12)
          }
        }, (ticket: Ticket) => JSON.stringify([ticket.id, ticket.title, ticket.status]))
      }.layoutWeight(1)
      if (this.selectedId.length > 0) {
        Text(`编辑记录:${this.selectedId}`)
        TextInput({ text: this.draftTitle, placeholder: '输入新标题' })
          .enabled(!this.busy)
          .onChange((value: string) => {
            this.draftTitle = value
          })
        Button('保存标题').enabled(!this.busy).onClick(() => {
          this.save()
        })
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

updated只在影响1行后设为true。之后查询失败,提示更新已完成但刷新失败,而不是笼统提示“保存失败”让用户反复提交。

普通Error不一定有平台错误码,要同时查看message。这里未提供持久化层的完整生命周期管理、冲突检测或事务方案,RdbStore的使用范围仍需在实际工程中规划。

写入已经完成但列表刷新失败时,要保留这个区别,不能让提示误导下一次操作。

写入完成与刷新失败分开

八、按加载、编辑、保存与重启路径核验

场景本例预期或需观察内容当前状态
首次加载查询后整体赋值tickets待验证
正常空表显示暂无记录待验证
查询失败显示错误,保留旧列表待验证
选择一条业务ID草稿显示该条标题,列表不立即改变待验证
空白或超长输入更新失败提示,不执行有效更新待验证
保存合法标题核对影响1行,重新查询并显示待验证
匹配不到ID影响行数异常,不宣称成功待验证
更新后查询失败提示更新已完成但刷新失败待验证
正常重启后只加载核对保存的标题仍能查询待验证

本篇没有自动插入数据。可在独立练习库中沿027建立自己的测试记录,再进行编辑。保留实际DevEco Studio、SDK/API、设备与完整日志,不把概念图当作运行截图。

以数据库读回值更新列表,并分别记录加载、写入和刷新阶段的结果。

记录完整编辑路径

写在最后

这一节我把数据库操作接到页面上:查询得到数组,数组进入状态,编辑草稿独立保存,更新按业务ID执行,成功后再查询。

下一篇学习异步函数与Promise基础,继续解释这些等待、返回与异常传播为什么影响页面结果。

参考资料

  1. OpenHarmony官方文档:@State装饰器。
  2. OpenHarmony官方文档:ForEach循环渲染。
  3. OpenHarmony官方文档:RdbStore接口。实际HarmonyOS工程以对应SDK资料为准。
  4. 本系列027数据库基础与028原稿《数据库记录列表展示与更新》,本篇计划日期2026-10-18。

声明:本文为个人学习笔记。代码与配图未经过SDK编译或设备实测,未操作真实业务数据库,运行结果需在独立练习工程核验。

结束分隔线

返回顶部

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐