在移动应用开发中,数据层的设计往往决定了应用的响应速度、稳定性和可维护性。鸿蒙系统的relationalStore作为本地关系型数据库解决方案,为开发者提供了高性能、低延迟的数据持久化能力。本文将从一个真实的饮品点餐应用场景出发,分享如何在不依赖网络请求的情况下,构建健壮、可扩展的本地数据管理架构。
离线优先(Offline-First)是一种应用架构理念,它假设网络连接是不可靠的,优先保证应用在无网络环境下的完整功能体验。这种架构特别适合餐饮零售、现场服务等需要快速响应的业务场景。
在饮品点餐应用中,我们需要处理三类核心数据实体:
传统做法可能会将这些数据分散存储在不同的模块中,但更好的做法是采用集中式的数据管理层。鸿蒙的relationalStore提供了完整的SQLite功能支持,包括事务、外键约束和索引等特性,非常适合构建这样的数据层。
typescript复制// 数据库初始化示例
const config = {
name: 'BeverageDB.db',
securityLevel: relationalStore.SecurityLevel.S1
}
relationalStore.getRdbStore(context, config, (err, rdbStore) => {
if(err) {
console.error('数据库初始化失败:', err)
return
}
// 初始化表结构
initTables(rdbStore)
})
合理的表结构设计是高效数据操作的基础。在饮品点餐场景中,我们需要建立用户、购物车和订单之间的关联关系。
| 表名 | 主键 | 外键 | 主要字段 | 说明 |
|---|---|---|---|---|
| user | Uno | - | Uname, Upassword | 用户基本信息 |
| cart | (Ctime, Uno) | Uno → user.Uno | Tname, Csum, Ccup, Ctemp, Csweetness | 购物车条目 |
| order | Bno | Uno → user.Uno | Btime, Btype, Bprice, Bdno, Bsum | 订单记录 |
外键约束能有效保证数据的完整性和一致性。例如,当删除用户时,数据库会自动级联删除该用户的购物车和订单记录:
sql复制CREATE TABLE cart (
Ctime TEXT,
Uno TEXT,
Tname TEXT,
Csum INTEGER,
Ccup INTEGER,
Ctemp INTEGER,
Csweetness INTEGER,
PRIMARY KEY (Ctime, Uno),
FOREIGN KEY (Uno) REFERENCES user(Uno) ON DELETE CASCADE
)
提示:在开发阶段开启外键约束需要显式设置PRAGMA foreign_keys = ON
直接操作数据库接口会导致业务代码与数据访问代码高度耦合。我们可以通过封装数据访问对象(DAO)来提高代码的可维护性。
鸿蒙的relationalStore API大多是异步的,我们需要妥善处理Promise链:
typescript复制class UserDAO {
private rdbStore: relationalStore.RdbStore
// 用户注册
async register(user: User): Promise<boolean> {
try {
const affectedRows = await this.rdbStore.insert('user', {
Uno: user.id,
Uname: user.name,
Upassword: user.password
})
return affectedRows > 0
} catch (err) {
console.error('注册失败:', err)
return false
}
}
// 用户登录
async login(userId: string, password: string): Promise<User | null> {
const predicates = new relationalStore.RdbPredicates('user')
predicates.equalTo('Uno', userId)
const result = await this.rdbStore.query(predicates, ['Uno', 'Uname'])
if (result.rowCount === 1) {
result.goToNextRow()
return {
id: result.getString(0),
name: result.getString(1)
}
}
return null
}
}
对于需要原子性保证的操作,如提交订单(从购物车移动到订单表),应该使用事务:
typescript复制async submitOrder(userId: string): Promise<boolean> {
return new Promise((resolve) => {
this.rdbStore.beginTransaction()
try {
// 1. 查询购物车所有商品
const cartItems = await this.getCartItems(userId)
// 2. 创建订单记录
const orderId = generateOrderId()
await this.createOrder(orderId, userId, cartItems)
// 3. 清空购物车
await this.clearCart(userId)
this.rdbStore.commit()
resolve(true)
} catch (err) {
this.rdbStore.rollback()
console.error('订单提交失败:', err)
resolve(false)
}
})
}
原始方案中将饮品数据硬编码在代码中虽然简单,但存在几个问题:
我们提出两种改进方案:
将饮品数据存储在应用的resources目录下:
code复制resources/
├─ rawfile/
│ ├─ beverages.json
│ ├─ beverages_en.json
json复制// beverages.json
[
{
"id": 1,
"name": "经典奶茶",
"price": 12.5,
"specs": {
"cup": ["中杯", "大杯"],
"temp": ["热", "冰"],
"sweetness": ["无糖", "微糖", "半糖", "全糖"]
}
}
]
加载时使用鸿蒙的资源管理器:
typescript复制async loadBeverages() {
try {
const rawFile = await getContext().resourceManager.getRawFile('beverages.json')
const content = String.fromCharCode.apply(null, new Uint8Array(rawFile))
return JSON.parse(content)
} catch (err) {
console.error('加载饮品数据失败:', err)
return []
}
}
对于更复杂的数据结构,可以在应用打包时预置完整的数据库文件:
typescript复制async initPresetDatabase() {
const dbPath = getDatabasePath('beverages.db')
if (!fileExists(dbPath)) {
const presetDb = await getContext().resourceManager.getRawFile('preset_beverages.db')
await writeFile(dbPath, presetDb)
}
return relationalStore.getRdbStore({
name: 'beverages.db',
path: dbPath
})
}
在实际开发中,我们积累了几个提升数据库性能的经验:
为常用查询条件添加索引可以显著提升查询速度:
sql复制-- 为用户的订单查询添加索引
CREATE INDEX idx_order_user ON order(Uno);
-- 为订单时间范围查询添加索引
CREATE INDEX idx_order_time ON order(Btime);
相比单条操作,批量操作能减少IO开销:
typescript复制async batchInsertCartItems(items: CartItem[]) {
const valueBuckets = items.map(item => ({
Ctime: item.time,
Uno: item.userId,
Tname: item.beverageName,
Csum: item.quantity,
Ccup: item.cupType,
Ctemp: item.temperature,
Csweetness: item.sweetness
}))
return this.rdbStore.batchInsert('cart', valueBuckets)
}
鸿蒙数据库调试的几个实用方法:
typescript复制try {
await dbOperation()
} catch (err) {
console.error(`操作失败: ${err.message}\nStack: ${err.stack}`)
if (err.code) {
console.error(`错误代码: ${err.code}`)
}
}
在开发那个饮品点餐应用时,最耗时的不是功能实现,而是排查一个由于字段名拼写错误导致的数据插入失败。后来我们建立了标准的字段命名规范和更完善的错误日志系统,类似问题再出现时就能快速定位了。