物品管理App通常需要保存物品名称、分类、数量、存放位置、备注以及创建时间等信息。如果仅依赖内存变量保存数据,应用关闭后数据就会丢失,显然无法满足实际使用需求。对于鸿蒙OS应用而言,将物品信息持久化到本地数据库,可以让用户随时查看和维护自己的物品清单。
鸿蒙OS提供了完善的本地数据存储能力,开发者可以根据数据规模和业务复杂度选择合适的存储方案。对于具有结构化数据、增删改查需求的物品管理App,关系型数据库更加适合。通过数据库保存物品记录,还可以进一步实现分类查询、关键词搜索、数量统计和数据排序等功能。
一、物品管理App为什么需要本地数据库
物品管理场景中的数据具有明显的结构化特点。例如一条物品记录可以包含以下字段:
id
name
category
quantity
location
remark
createTime
updateTime这些字段之间具有明确的数据关系,并且用户通常会执行新增、修改、删除和查询操作。
如果使用普通对象数组保存:
let items: Item[] = []虽然开发阶段实现简单,但应用进程退出以后,数组中的数据也会随之消失。因此,需要增加持久化存储层。
本地数据库具有几个明显优势:
数据可以长期保存。
支持条件查询和排序。
可以按照业务需求设计数据表。
数据量增加后仍然具有较好的管理能力。
UI层与数据层可以进行职责分离,便于后续维护。
对于不需要服务器同步的个人物品管理App,本地数据库尤其适合保存用户自己的物品数据。
二、设计物品数据表
数据库设计直接影响后续开发难度。物品管理功能虽然看起来简单,但仍然建议提前确定字段。
一个比较实用的物品表可以设计成:
| 字段 | 类型 | 作用 |
|---|---|---|
| id | INTEGER | 物品唯一编号 |
| name | TEXT | 物品名称 |
| category | TEXT | 物品分类 |
| quantity | INTEGER | 数量 |
| location | TEXT | 存放位置 |
| remark | TEXT | 备注信息 |
| createTime | INTEGER | 创建时间 |
| updateTime | INTEGER | 修改时间 |
其中id可以作为主键,避免不同物品记录之间产生冲突。
quantity建议使用整数类型,而不是直接保存为字符串。这样后续进行库存统计、数量排序时更加方便。
时间字段可以使用时间戳保存。例如:
const now = Date.now()这种方式便于按照创建时间或者修改时间进行排序。
三、创建数据库访问层
实际项目中,不建议在页面代码中直接编写大量数据库操作。更合理的方式是单独创建数据库管理类,将数据库初始化、建表以及增删改查操作集中管理。
例如可以设计一个ItemRepository:
export class ItemRepository {
private store: relationalStore.RdbStore | undefined
async init(context: Context) {
const config: relationalStore.StoreConfig = {
name: 'item_manager.db',
securityLevel: relationalStore.SecurityLevel.S1
}
this.store = await relationalStore.getRdbStore(context, config)
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT,
quantity INTEGER DEFAULT 1,
location TEXT,
remark TEXT,
createTime INTEGER,
updateTime INTEGER
)
`)
}
}实际开发时需要根据所使用的HarmonyOS SDK版本调整相关API及类型定义。不同版本的鸿蒙开发环境在数据库接口细节上可能存在差异,因此应以当前SDK提供的官方API为准。
这种封装方式最大的好处是页面不需要关心数据库连接细节。
四、初始化本地数据库
数据库初始化一般可以放在App启动后的数据层初始化逻辑中。
基本流程可以概括为:
应用启动
↓
获取Context
↓
创建或打开数据库
↓
检查数据表
↓
创建items表
↓
Repository准备完成
↓
页面开始读取数据数据库文件不存在时,系统会根据配置创建数据库;如果数据库已经存在,则直接打开。
需要特别注意初始化时机。如果页面加载完成后数据库还没有准备好,就立即执行查询,很容易出现空对象、数据库连接未建立等问题。
因此可以采用统一的异步初始化方式:
await itemRepository.init(context)
const items = await itemRepository.findAll()确保数据库初始化成功以后,再向UI层提供数据。
五、实现物品新增功能
新增物品时,需要将页面表单中的数据转换成数据库记录。
例如用户填写:
名称:无线键盘
分类:电子产品
数量:2
位置:书房
备注:办公使用可以构造对应的数据对象:
const now = Date.now()
const values = {
name: '无线键盘',
category: '电子产品',
quantity: 2,
location: '书房',
remark: '办公使用',
createTime: now,
updateTime: now
}然后通过数据库接口执行插入操作。
推荐使用参数绑定,而不是直接拼接SQL字符串:
const predicates = new relationalStore.RdbPredicates('items')
await store.insert('items', values)如果使用条件语句进行查询、修改或者删除,也应尽量使用参数绑定机制。这样不仅代码更加清晰,也能够降低字符串拼接带来的SQL注入风险。
六、查询物品列表
物品列表是管理App最核心的功能之一。
最简单的查询是获取所有物品:
const predicates = new relationalStore.RdbPredicates('items')
const resultSet = await store.query(
predicates,
['id', 'name', 'category', 'quantity', 'location', 'remark', 'createTime', 'updateTime']
)查询结果通常需要从结果集中读取字段,再转换成应用层对象。
可以统一定义:
export interface Item {
id: number
name: string
category: string
quantity: number
location: string
remark: string
createTime: number
updateTime: number
}这样数据库对象与UI组件之间就有了明确的数据结构。
为了避免页面直接操作ResultSet,建议Repository层完成结果转换:
数据库ResultSet
↓
Repository
↓
Item[]
↓
ViewModel
↓
UI这种分层结构能够降低数据库API与页面代码之间的耦合。
七、实现物品搜索
当物品数量增加以后,仅仅显示全部数据已经不够实用。可以增加关键词搜索功能。
例如用户输入“键盘”,数据库只返回名称中包含“键盘”的记录。
查询条件可以使用:
name LIKE '%键盘%'实际实现时,应使用数据库提供的参数绑定机制,不建议将用户输入直接拼接到SQL语句中。
搜索逻辑可以设计成:
用户输入关键词
↓
输入框状态变化
↓
调用Repository.search()
↓
数据库执行条件查询
↓
返回匹配结果
↓
刷新列表如果数据量较大,不建议每输入一个字符都立即执行复杂查询,可以增加简单的防抖机制,减少数据库访问次数。
八、实现分类筛选
物品管理通常还需要按照分类进行筛选,例如:
全部
电子产品
生活用品
办公用品
工具
其他数据库查询可以通过分类字段完成。
当用户选择“办公用品”时,只需要增加对应条件:
const predicates = new relationalStore.RdbPredicates('items')
predicates.equalTo('category', '办公用品')如果同时存在关键词和分类筛选,可以组合多个条件:
category = 办公用品
AND
name LIKE 关键词这种查询方式比先查询全部数据,再由ArkTS代码过滤更加合理,尤其是在数据量较大的情况下,可以减少不必要的数据读取。
九、实现修改物品
用户进入物品详情页后,可以修改名称、分类、数量、位置和备注。
修改数据库记录时,关键是通过唯一ID定位目标数据:
const predicates = new relationalStore.RdbPredicates('items')
predicates.equalTo('id', itemId)
const values = {
name: '新的物品名称',
quantity: 3,
updateTime: Date.now()
}
await store.update(values, predicates)这里不建议使用物品名称作为更新条件,因为名称可能重复。
例如数据库中可能同时存在:
无线鼠标
无线鼠标如果使用名称更新,很容易误修改多条记录。
因此应该始终优先使用主键ID定位数据。
十、实现删除物品
删除操作通常需要用户进行二次确认,避免误操作。
数据库层面可以根据ID删除:
const predicates = new relationalStore.RdbPredicates('items')
predicates.equalTo('id', itemId)
await store.delete(predicates)页面逻辑可以设计为:
点击删除
↓
显示确认弹窗
↓
用户确认
↓
Repository.delete(id)
↓
数据库删除记录
↓
刷新列表删除完成后,应同步更新页面中的列表数据,否则数据库虽然已经删除记录,界面可能仍然显示旧数据。
十一、使用事务保证数据一致性
如果一次操作需要修改多张表或者执行多条SQL语句,就应该考虑事务。
例如物品管理App以后增加“出入库记录”功能,用户减少库存的同时还需要写入一条操作日志:
更新物品数量
+
新增库存操作记录如果第一步成功、第二步失败,就会产生数据不一致。
事务可以将多个操作放到一个完整的执行单元中:
BEGIN
↓
更新库存
↓
写入操作记录
↓
COMMIT如果中间发生异常:
ROLLBACK这样可以避免出现库存已经修改,但操作记录没有保存的情况。
对于简单的单表增删改操作,不一定需要人为增加复杂事务,但涉及多个数据表时应认真考虑事务边界。
十二、数据库版本升级问题
App上线以后,数据库结构并不是永远不变。
例如第一版只有:
id
name
quantity第二版增加:
location
remark
updateTime如果直接修改数据库结构,旧用户升级以后可能出现数据表结构不匹配。
因此数据库应该设计版本管理机制。
升级过程可以理解为:
数据库版本1
↓
检测当前版本
↓
执行升级脚本
↓
增加新字段或新表
↓
更新数据库版本号
↓
数据库版本2数据库升级必须保证向后兼容,尤其要避免升级脚本重复执行导致异常。
对于正式项目,建议从第一版开始就考虑数据库版本号,而不是等到数据结构发生变化后再补救。
十三、避免在UI线程中进行大量数据库操作
物品管理App的数据量可能不大,但数据库操作仍然应该保持合理的异步调用方式。
例如不应该在页面渲染逻辑中反复执行数据库查询:
UI刷新
↓
查询数据库
↓
UI刷新
↓
再次查询数据库这样容易造成不必要的性能开销。
更合理的方式是让数据层负责获取数据,然后由状态管理机制通知UI:
Database
↓
Repository
↓
ViewModel / 状态层
↓
UI当数据库发生变化后,更新对应的数据状态,页面根据状态重新渲染。
这种架构不仅适用于物品管理App,也适用于通讯录、记账、待办事项、库存管理等大量鸿蒙OS应用场景。
十四、做好空数据和异常处理
数据库操作并不意味着每次都能成功。
实际运行过程中可能遇到数据库初始化失败、查询异常、字段类型错误或者数据为空等情况。
例如查询列表时,需要正确处理:
数据库没有记录这种情况不应该直接视为异常,而应该向用户展示合理的空状态,例如:
暂无物品
点击右下角按钮添加第一件物品对于真正的数据库异常,则应该记录日志并向用户提供友好的错误提示,而不是直接将底层异常信息展示出来。
例如:
try {
const items = await itemRepository.findAll()
this.items = items
} catch (error) {
console.error(`查询物品失败: ${JSON.stringify(error)}`)
}生产环境还可以进一步增加统一错误处理和日志上报机制。
十五、推荐的项目目录结构
为了让鸿蒙OS物品管理App更加容易维护,可以采用类似下面的目录结构:
entry/
├── src/
│ └── main/
│ ├── ets/
│ │ ├── pages/
│ │ │ ├── Index.ets
│ │ │ ├── AddItem.ets
│ │ │ └── ItemDetail.ets
│ │ ├── model/
│ │ │ └── Item.ets
│ │ ├── repository/
│ │ │ └── ItemRepository.ets
│ │ ├── viewmodel/
│ │ │ └── ItemViewModel.ets
│ │ └── database/
│ │ └── DatabaseManager.ets其中:
model负责定义物品数据模型。database负责数据库初始化和底层配置。repository负责数据库增删改查。viewmodel负责业务状态管理。pages负责页面展示和用户交互。
这种结构能够避免把所有逻辑都堆积到一个.ets页面文件中。
十六、进一步优化本地数据库设计
当基础功能完成以后,可以继续从几个方面优化。
第一,给经常用于查询的字段建立合理索引。例如分类、名称等字段在数据量较大时可以考虑索引。
第二,避免保存不必要的数据。物品照片等大体积内容不适合直接作为普通字段大量存储,可以结合文件存储能力保存图片,再让数据库记录文件路径或标识。
第三,对列表查询进行分页。物品达到几千甚至更多时,一次性加载全部记录会增加内存和渲染压力。
第四,对输入数据进行校验。例如物品名称不能为空,数量不能为负数。
第五,统一数据库访问入口。不要让多个页面分别创建数据库连接和执行SQL,否则后期很难维护。
十七、一个完整的数据流设计
一个较为成熟的鸿蒙OS物品管理App,可以采用以下数据流:
用户操作
↓
ArkUI页面
↓
ViewModel
↓
ItemRepository
↓
本地关系型数据库
↓
查询/新增/修改/删除
↓
Repository转换数据
↓
ViewModel更新状态
↓
ArkUI重新渲染新增物品时,数据从UI进入数据库;查询物品时,数据则从数据库返回UI。
这种单向、清晰的数据流可以降低组件之间的耦合,也方便以后增加云同步、数据导入导出等功能。
十八、总结
鸿蒙OS物品管理App采用本地数据库保存数据,可以有效解决应用关闭后物品信息丢失的问题。对于具有结构化数据和增删改查需求的应用,关系型数据库能够提供更加完善的数据管理能力。
实现过程中,重点并不只是完成“数据保存”,还应该关注数据库表结构设计、Repository封装、异步操作、条件查询、事务、异常处理以及数据库版本升级。
一个合理的实现方案可以归纳为:
合理设计数据表
↓
统一初始化数据库
↓
封装Repository
↓
实现增删改查
↓
增加搜索和分类筛选
↓
处理异常与空数据
↓