杨利杰YJlio博客头图

关系型数据库保存业务数据

大家好,我是杨利杰 YJlio。

前两节分别整理了设置与文件。这一节我用最小工单表练习业务记录的保存:先准备表,再插入一条记录,查询、更新,最后按指定ID删除练习记录。

本篇编号027,计划日期2026年10月17日。原稿已保留。原稿loadTickets只是返回空数组的占位,下面补充实际接口调用的学习代码;尚未在本机编译或设备运行,配图均为概念示意。

一、先把业务字段定下来

原稿的Ticket有id、title、status三个字符串字段。我保留这组结构,并为数据库列确定名称:

interface Ticket {
  id: string;
  title: string;
  status: string;
}
业务字段数据库列本例类型与约束
idIDTEXT,非空主键
titleTITLETEXT,非空
statusSTATUSTEXT,非空

T-001、打印机离线、new都是虚构练习值,不对应真实工单。status继续使用原稿的string,完整业务应用还需规定允许值与流转规则。

先把工单字段定下来

二、先取得RdbStore,再调用数据接口

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

async function openTicketStore(
  context: common.UIAbilityContext
): Promise<relationalStore.RdbStore> {
  const config: relationalStore.StoreConfig = {
    name: 'TicketLearn.db',
    securityLevel: relationalStore.SecurityLevel.S3
  };
  return await relationalStore.getRdbStore(context, config);
}

context应来自当前可用的UIAbility或页面宿主。TicketLearn.db是本例单独使用的学习库名称,不指向其他业务数据库。

securityLevel是存储配置的一部分,不等于已经启用了数据库加密;涉及真实数据时还需按业务敏感程度与对应SDK核对配置。本例不加入任何实际个人信息。

先取得RdbStore再操作

三、首次建表与后续升级分开

async function prepareTicketTable(store: relationalStore.RdbStore): Promise<void> {
  await store.executeSql(
    'CREATE TABLE IF NOT EXISTS TICKET (' +
    'ID TEXT PRIMARY KEY NOT NULL, ' +
    'TITLE TEXT NOT NULL, ' +
    'STATUS TEXT NOT NULL)'
  );
}

getRdbStore完成后,还要等待prepareTicketTable完成,再执行后面的增删查改。

IF NOT EXISTS用于避免表已存在时重复创建,不会自动把旧表更新为新的字段结构。这篇只提供第一次学习建表,不实现完整版本迁移;已有数据的工程应另做升级与回滚设计。

能打开数据库,不代表目标表已经准备好;建表也不等于完成结构迁移。

首次建表与升级分开

四、用ValuesBucket新增一条记录

async function addTicket(
  store: relationalStore.RdbStore,
  ticket: Ticket
): Promise<number> {
  if (ticket.id.trim().length === 0 || ticket.title.trim().length === 0) {
    throw new Error('工单ID与标题不能为空');
  }
  const values: relationalStore.ValuesBucket = {
    ID: ticket.id,
    TITLE: ticket.title,
    STATUS: ticket.status
  };
  const rowId = await store.insert('TICKET', values);
  if (rowId < 0) {
    throw new Error('插入工单失败');
  }
  return rowId;
}

调用示例使用{id: ‘T-001’, title: ‘打印机离线’, status: ‘new’}。参数通过ValuesBucket交给接口,不拼接进SQL语句。

insert返回的是数值行号,本例业务ID仍是字符串T-001。这两者用途不同,不能把返回的number直接塞进Ticket.id。

重复插入相同主键可能失败。本例没有采用覆盖冲突策略,也不会为了重试而清空整个数据库。

新增用ValuesBucket

五、查询转成数组,finally关闭结果集

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

查询成功且没有记录时,这个函数才返回空数组。query失败或字段读取失败应交给调用方处理,不能统一catch后return [],否则界面会把故障显示成“没有工单”。

本例只面向少量学习记录,没有分页、排序和完整列表界面。记录较多时应限制查询范围,不把一次性全表读取扩展为生产方案。

读完结果集要关闭

六、更新和删除都带业务ID条件

async function changeTicketStatus(
  store: relationalStore.RdbStore,
  id: string,
  status: string
): Promise<number> {
  if (id.trim().length === 0) {
    throw new Error('工单ID不能为空');
  }
  const predicates = new relationalStore.RdbPredicates('TICKET');
  predicates.equalTo('ID', id);
  const values: relationalStore.ValuesBucket = { STATUS: status };
  return await store.update(values, predicates);
}

async function removeTicket(
  store: relationalStore.RdbStore,
  id: string
): Promise<number> {
  if (id.trim().length === 0) {
    throw new Error('工单ID不能为空');
  }
  const predicates = new relationalStore.RdbPredicates('TICKET');
  predicates.equalTo('ID', id);
  return await store.delete(predicates);
}

更新示例把T-001状态改为processing。删除函数只用于自己创建的学习记录;本文没有执行任何数据库删除操作。

两个接口的返回值表示影响行数。0不等于接口一定抛异常,可能只是没有找到该ID。对于本例单行主键操作,应核对是否影响1行,再决定界面反馈。

更新与删除条件要明确,返回的影响行数也要检查。

更新删除都限定ID

七、失败由调用方反馈,不自动清库

初始化、建表、插入与查询都可能失败。调用方捕获错误后应记录阶段和完整错误,不显示虚构的“保存成功”。例如只插入和查询的调用函数:

import { BusinessError } from '@kit.BasicServicesKit';

async function insertAndRead(context: common.UIAbilityContext): Promise<Ticket[]> {
  try {
    const store = await openTicketStore(context);
    await prepareTicketTable(store);
    await addTicket(store, { id: 'T-001', title: '打印机离线', status: 'new' });
    return await loadTickets(store);
  } catch (error) {
    const err = error as BusinessError;
    console.error(`insertAndRead failed: ${err.code}, ${err.message}`);
    throw error;
  }
}

这是一次性插入练习,反复调用可能遇到主键冲突。错误没有转为空列表,页面还需要自己的失败提示和忙碌状态。普通Error不一定带平台错误码,应同时查看message。

代码块属于同一组函数与类型声明;在实际文件中整理导入,不重复粘贴声明。本篇不实现完整页面、并发控制、版本迁移或多步骤事务服务;RdbStore实例的持有与释放要按实际使用生命周期安排,不能在其他调用仍使用时关闭。

初始化和写入失败都要反馈

八、沿增删查改路径核对结果

步骤预期或需检查内容当前状态
打开TicketLearn.db并建表完成后再操作TICKET待验证
插入T-001数值行号有效,业务ID仍为T-001待验证
查询ID、TITLE、STATUS映射到Ticket待验证
更新状态为processing核对影响1行,再查询状态待验证
正常重启后只查询在删除前核对记录是否仍在待验证
删除学习记录T-001核对影响1行,再查询确认待验证
重复插入或异常输入捕获错误,不把失败当空列表待验证

重启核验时不要再次调用insertAndRead,应打开同名库、准备表后只调用loadTickets。保留实际DevEco Studio、SDK/API、设备与完整日志。本文函数采用Promise形式,需与工程的接口重载匹配。

示例中的行号、影响行数和重启读回都是检查目标,实际结果仍需设备验证。

按完整路径核对数据

写在最后

这篇我把原稿的空数组占位补成了数据库操作路径:业务字段对应列,表准备好再写入,查询结果集及时关闭,更新和删除限定业务ID。

下一篇继续处理数据库记录的列表展示与更新,重点放在持久化结果如何回到页面状态。

参考资料

  1. OpenHarmony官方文档:关系型数据库持久化,用于核对基本流程。
  2. OpenHarmony官方文档:RdbStore接口,用于核对executeSql、insert、query、update、delete;实际HarmonyOS工程以对应SDK资料为准。
  3. 本系列第027篇原稿:《用关系型数据库保存业务数据》,计划日期2026-10-17。

声明:本文为个人学习笔记。数据为虚构演示值,代码和配图未经过SDK编译或设备实测,未操作真实业务数据库。

结束分隔线

返回顶部

Logo

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

更多推荐