前言
前面我们用 HTTP 拉排行榜、上报得分——但应用关闭后本地数据全没了,下次开要重新拉。战绩历史、玩家昵称、皮肤解锁这种结构化持久数据要存本地数据库。HarmonyOS 提供@ohos.data.relationalStore(SQLite 封装)做关系型存储——建表、插入、查询、更新、删除,SQL 语法齐全。
本篇以「猫猫大作战」战绩历史表(建表 + 插入 + 查询 Top 50 + 删除)为锚点,把relationalStore 建库建表、增删改查 SQL、事务批量插入、与 Preference 键值取舍四大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–63 篇。本篇是阶段四第四篇。
一、场景拆解:战绩历史持久化
「猫猫大作战」战绩历史(第 47、48 篇 LazyForEach 用过GameRecord[])当前是内存数组——应用关就没了。玩家想本地持久化战绩:
- 每局结束插入一条记录(得分/时间/连击/日期)。
- 战绩页查询最近 50 条(按日期降序)。
- 删除单条战绩(误录清除)。
- 批量插入历史导入(从服务器拉 100 条一次写入)。
// 预演:HistoryDb 建表 + CRUD import { relationalStore } from '@kit.ArkData'; export class HistoryDb { private store: relationalStore.RdbStore | null = null; private readonly dbName: string = 'catgame.db'; private readonly tableName: string = 'history'; async init(context: Context): Promise<void> { const config: relationalStore.StoreConfig = { name: this.dbName, securityLevel: relationalStore.SecurityLevel.S1 }; this.store = await relationalStore.getRdbStore(context, config); await this.createTable(); } private async createTable(): Promise<void> { const sql = `CREATE TABLE IF NOT EXISTS ${this.tableName} ( id INTEGER PRIMARY KEY AUTOINCREMENT, score INTEGER NOT NULL, duration INTEGER NOT NULL, max_combo INTEGER NOT NULL, merge_count INTEGER NOT NULL, highest_level INTEGER NOT NULL, date TEXT NOT NULL )`; await this.store!.executeSql(sql); } // 插入 async insert(record: Omit<GameRecord, 'id'>): Promise<number> { const values: relationalStore.ValuesBucket = { 'score': record.score, 'duration': record.duration, 'max_combo': record.maxCombo, 'merge_count': record.mergeCount, 'highest_level': record.highestLevel, 'date': record.date }; const rowId = await this.store!.insert(this.tableName, values); return rowId; } // 查询最近 50 条 async queryRecent(limit: number = 50): Promise<GameRecord[]> { const sql = `SELECT * FROM ${this.tableName} ORDER BY date DESC LIMIT ?`; const result = await this.store!.querySql(sql, [limit]); const records: GameRecord[] = []; while (result.goToNextRow()) { records.push(this.mapRow(result)); } return records; } // 删除单条 async delete(id: number): Promise<void> { const predicates = new relationalStore.RdbPredicates(this.tableName); predicates.equalTo('id', id); await this.store!.delete(predicates); } private mapRow(result: relationalStore.ResultSet): GameRecord { return { id: result.getLong(result.getColumnIndex('id')), score: result.getLong(result.getColumnIndex('score')), duration: result.getLong(result.getColumnIndex('duration')), maxCombo: result.getLong(result.getColumnIndex('max_combo')), mergeCount: result.getLong(result.getColumnIndex('merge_count')), highestLevel: result.getLong(result.getColumnIndex('highest_level')), date: result.getString(result.getColumnIndex('date')) }; } }核心问题:
relationalStore怎么建库建表?- 增删改查四种 SQL 怎么用?参数化查询怎么防注入?
- 事务怎么批量插入?
- SQLite 和 Preference 键值存储怎么选?
二、relationalStore 建库建表
2.1 申请数据存储权限
修改entry/src/main/module.json5:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.data.relationalStore", "reason": "$string:reason_database", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } }提示:某些 HarmonyOS 版本 relationalStore 不需要单独权限——应用沙盒内数据库自动授权。以实际 SDK 为准,权限报错时补申请。
2.2 创建 RdbStore
import { relationalStore } from '@kit.ArkData'; const config: relationalStore.StoreConfig = { name: 'catgame.db', // 数据库文件名 securityLevel: relationalStore.SecurityLevel.S1 // 安全等级 }; const store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, config);config 字段:
| 字段 | 含义 | 推荐值 |
|---|---|---|
name | 数据库文件名(.db) | <app>.db |
securityLevel | 安全等级(S1–S4) | S1(最低,普通数据) |
安全等级:
| 等级 | 含义 | 适合 |
|---|---|---|
S1 | 开放(低) | 公开数据(排行榜缓存) |
S2 | 普通 | 用户数据(战绩、昵称) |
S3 | 个人敏感 | 身份信息 |
S4 | 严格机密 | 密码、财务 |
关键经验:「战绩/昵称」用 S2,「公开缓存」用 S1——不要全部 S4,否则备份/迁移受限。
2.3 建表 SQL
const sql = `CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, score INTEGER NOT NULL, duration INTEGER NOT NULL, max_combo INTEGER NOT NULL, merge_count INTEGER NOT NULL, highest_level INTEGER NOT NULL, date TEXT NOT NULL )`; await store.executeSql(sql);SQL 拆解:
| 片段 | 含义 |
|---|---|
CREATE TABLE IF NOT EXISTS | 建表,存在则跳过(幂等) |
history | 表名 |
id INTEGER PRIMARY KEY AUTOINCREMENT | 主键自增 |
score INTEGER NOT NULL | 整数字段,非空 |
date TEXT NOT NULL | 文本字段,非空 |
关键经验:建表用IF NOT EXISTS——重启应用多次建表幂等,不报错。
2.4 SQLite 字段类型
SQLite 是动态类型,但建表仍推荐声明:
| 声明 | 实际存储 | 适合 |
|---|---|---|
INTEGER | 整数 | 得分、时长、连击 |
TEXT | 文本 | 日期、昵称、UUID |
REAL | 浮点 | 评分、坐标 |
BLOB | 二进制 | 头像、序列化数据 |
三、增删改查 SQL
3.1 增:insert
// 方式 A:API insert(推荐,防注入) const values: relationalStore.ValuesBucket = { 'score': 1500, 'duration': 180, 'max_combo': 5, 'merge_count': 28, 'highest_level': 3, 'date': '2026-07-24T10:30:00Z' }; const rowId = await store.insert('history', values); // 方式 B:原始 SQL(要手动参数化防注入) const sql = `INSERT INTO history (score, duration, max_combo, merge_count, highest_level, date) VALUES (?, ?, ?, ?, ?, ?)`; await store.executeSql(sql, [1500, 180, 5, 28, 3, '2026-07-24T10:30:00Z']);关键经验:插入用store.insert(tableName, values)API——自动参数化防注入,不用拼 SQL。
3.2 查:querySql
// 查询最近 50 条(参数化) const sql = `SELECT * FROM history ORDER BY date DESC LIMIT ?`; const result = await store.querySql(sql, [50]); const records: GameRecord[] = []; while (result.goToNextRow()) { records.push({ id: result.getLong(result.getColumnIndex('id')), score: result.getLong(result.getColumnIndex('score')), /* ... */ }); } return records;ResultSet 遍历:
| API | 作用 |
|---|---|
result.goToNextRow() | 移到下一行,返回 false 表示结束 |
result.getColumnIndex('name') | 取字段索引 |
result.getLong(idx)/getString(idx) | 取字段值 |
关键经验:查询用querySql+ 参数化——LIMIT ?配[50],不拼字符串防注入。
3.3 改:update
// 方式 A:RdbPredicates(推荐) const predicates = new relationalStore.RdbPredicates('history'); predicates.equalTo('id', 5); const values: relationalStore.ValuesBucket = { 'score': 2000 }; const changedRows = await store.update(predicates, values); // 方式 B:原始 SQL const sql = `UPDATE history SET score = ? WHERE id = ?`; await store.executeSql(sql, [2000, 5]);3.4 删:delete
// 方式 A:RdbPredicates(推荐) const predicates = new relationalStore.RdbPredicates('history'); predicates.equalTo('id', 5); const deletedRows = await store.delete(predicates); // 方式 B:原始 SQL const sql = `DELETE FROM history WHERE id = ?`; await store.executeSql(sql, [5]);3.5 RdbPredicates 条件构造
const predicates = new relationalStore.RdbPredicates('history'); // 等于 predicates.equalTo('score', 1500); // 大于/小于 predicates.greaterThan('score', 1000); predicates.lessThan('duration', 300); // 范围 predicates.between('score', 1000, 2000); // 模糊匹配 predicates.like('date', '2026-07-%'); // 排序 + 分页 predicates.orderByAsc('date'); // 升序 predicates.orderByDesc('score'); // 降序 predicates.limitAs(50); // 取 50 条 predicates.offsetAs(10); // 跳过 10 条关键经验:RdbPredicates 链式构造条件——比手写 SQL 安全(自动参数化),可读性强。
四、事务批量插入
4.1 为什么要事务
场景:从服务器拉 100 条历史导入本地——单条 insert 100 次,中途某条失败要回滚已插的,否则数据不一致。
事务保证:要么全成功,要么全回滚。
4.2 事务 API
await store.beginTransaction(); // 开启事务 try { for (const record of records) { await store.insert('history', { 'score': record.score, 'duration': record.duration, /* ... */ }); } await store.commit(); // 提交(全成功才持久化) } catch (error) { await store.rollBack(); // 回滚(失败恢复到事务前) throw error; }4.3 性能对比
| 方式 | 100 条插入耗时 | 原因 |
|---|---|---|
| 单条 insert × 100 | ~2s | 每条都触发磁盘 IO + 索引更新 |
| 事务批量 | ~50ms | 一次磁盘 IO,索引一次更新 |
关键经验:批量插入必用事务——性能提升 40 倍,且保证原子性。
五、实战:HistoryDb 完整实现
5.1 创建 HistoryDb
新建entry/src/main/ets/services/HistoryDb.ets:
import { relationalStore } from '@kit.ArkData'; import { GameRecord, CatLevel } from '../components/GameTypes'; export class HistoryDb { private store: relationalStore.RdbStore | null = null; private readonly dbName: string = 'catgame.db'; private readonly tableName: string = 'history'; async init(context: Context): Promise<void> { const config: relationalStore.StoreConfig = { name: this.dbName, securityLevel: relationalStore.SecurityLevel.S2 }; this.store = await relationalStore.getRdbStore(context, config); await this.createTable(); } private async createTable(): Promise<void> { const sql = `CREATE TABLE IF NOT EXISTS ${this.tableName} ( id INTEGER PRIMARY KEY AUTOINCREMENT, score INTEGER NOT NULL, duration INTEGER NOT NULL, max_combo INTEGER NOT NULL, merge_count INTEGER NOT NULL, highest_level INTEGER NOT NULL, date TEXT NOT NULL )`; await this.store!.executeSql(sql); } // 插入单条 async insert(record: Omit<GameRecord, 'id'>): Promise<number> { const values: relationalStore.ValuesBucket = { 'score': record.score, 'duration': record.duration, 'max_combo': record.maxCombo, 'merge_count': record.mergeCount, 'highest_level': record.highestLevel, 'date': record.date }; return await this.store!.insert(this.tableName, values); } // 事务批量插入(服务器导入用) async batchInsert(records: Omit<GameRecord, 'id'>[]): Promise<void> { await this.store!.beginTransaction(); try { for (const record of records) { const values: relationalStore.ValuesBucket = { 'score': record.score, 'duration': record.duration, 'max_combo': record.maxCombo, 'merge_count': record.mergeCount, 'highest_level': record.highestLevel, 'date': record.date }; await this.store!.insert(this.tableName, values); } await this.store!.commit(); } catch (error) { await this.store!.rollBack(); throw error; } } // 查询最近 N 条(战绩页用) async queryRecent(limit: number = 50, offset: number = 0): Promise<GameRecord[]> { const predicates = new relationalStore.RdbPredicates(this.tableName); predicates.orderByDesc('date'); predicates.limitAs(limit); predicates.offsetAs(offset); const result = await this.store!.query(predicates); return this.mapResultSet(result); } // 查询最高分(统计) async queryHighestScore(): Promise<number> { const sql = `SELECT MAX(score) as max_score FROM ${this.tableName}`; const result = await this.store!.querySql(sql); if (result.goToNextRow()) { return result.getLong(result.getColumnIndex('max_score')) || 0; } return 0; } // 查询总场数 async queryTotalCount(): Promise<number> { const sql = `SELECT COUNT(*) as total FROM ${this.tableName}`; const result = await this.store!.querySql(sql); if (result.goToNextRow()) { return result.getLong(result.getColumnIndex('total')); } return 0; } // 按得分范围查询 async queryByScoreRange(min: number, max: number): Promise<GameRecord[]> { const predicates = new relationalStore.RdbPredicates(this.tableName); predicates.between('score', min, max); predicates.orderByDesc('score'); const result = await this.store!.query(predicates); return this.mapResultSet(result); } // 更新单条 async update(id: number, updates: Partial<GameRecord>): Promise<number> { const predicates = new relationalStore.RdbPredicates(this.tableName); predicates.equalTo('id', id); const values: relationalStore.ValuesBucket = {}; if (updates.score !== undefined) values['score'] = updates.score; if (updates.duration !== undefined) values['duration'] = updates.duration; if (updates.maxCombo !== undefined) values['max_combo'] = updates.maxCombo; if (updates.mergeCount !== undefined) values['merge_count'] = updates.mergeCount; if (updates.highestLevel !== undefined) values['highest_level'] = updates.highestLevel; return await this.store!.update(predicates, values); } // 删除单条 async delete(id: number): Promise<number> { const predicates = new relationalStore.RdbPredicates(this.tableName); predicates.equalTo('id', id); return await this.store!.delete(predicates); } // 删除所有(清空表) async deleteAll(): Promise<number> { const predicates = new relationalStore.RdbPredicates(this.tableName); return await this.store!.delete(predicates); } // 删除 30 天前的旧记录(清理) async deleteOlderThan(days: number): Promise<number> { const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); const sql = `DELETE FROM ${this.tableName} WHERE date < ?`; return await this.store!.executeSql(sql, [cutoffDate]); } private mapResultSet(result: relationalStore.ResultSet): GameRecord[] { const records: GameRecord[] = []; while (result.goToNextRow()) { records.push(this.mapRow(result)); } return records; } private mapRow(result: relationalStore.ResultSet): GameRecord { return { id: result.getLong(result.getColumnIndex('id')), score: result.getLong(result.getColumnIndex('score')), duration: result.getLong(result.getColumnIndex('duration')), maxCombo: result.getLong(result.getColumnIndex('max_combo')), mergeCount: result.getLong(result.getColumnIndex('merge_count')), highestLevel: result.getLong(result.getColumnIndex('highest_level')) as CatLevel, date: result.getString(result.getColumnIndex('date')) }; } // 销毁(关数据库连接) async close(): Promise<void> { if (this.store) { this.store = null; // 简化:实际可能有 release/close API } } }5.2 Index 结束游戏时插入战绩
// 来源:entry/src/main/ets/pages/Index.ets endGame(SQLite 改造后) import { HistoryDb } from '../services/HistoryDb'; import { common } from '@kit.AbilityKit'; @Entry @Component struct Index { /* ... 其他 state */ private historyDb: HistoryDb = new HistoryDb(); private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; aboutToAppear() { this.historyDb.init(this.context); // 初始化数据库 } endGame() { this.gameState = GameState.GAME_OVER; this.maxCombo = this.gameEngine.getMaxCombo(); this.mergeCount = this.gameEngine.getMergeCount(); this.highestLevel = this.gameEngine.getHighestLevel(); if (this.score > this.highScore) { this.highScore = this.score; } this.clearTimers(); // 异步插入战绩到 SQLite(不阻塞 UI) this.saveHistory(); } async saveHistory() { try { await this.historyDb.insert({ score: this.score, duration: this.gameTime, maxCombo: this.maxCombo, mergeCount: this.mergeCount, highestLevel: this.highestLevel, date: new Date().toISOString() }); console.info('战绩保存成功'); } catch (error) { console.error(`战绩保存失败: ${(error as Error).message}`); } } aboutToDisappear() { this.clearTimers(); this.historyDb.close(); } /* ... 其他方法 */ }5.3 改造 LeaderboardPage 显示本地战绩
// 来源:entry/src/main/ets/pages/LeaderboardPage.ets(SQLite 改造后) import { GameRecord, CatLevel } from '../components/GameTypes'; import { HttpService } from '../services/HttpService'; import { HistoryDb } from '../services/HistoryDb'; import { common } from '@kit.AbilityKit'; @Component export struct LeaderboardPage { @State history: GameRecord[] = []; @State loading: boolean = false; @State errorMsg: string = ''; private httpService: HttpService = new HttpService(); private historyDb: HistoryDb = new HistoryDb(); private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; aboutToAppear() { this.historyDb.init(this.context).then(() => { this.loadHistory(); }); } aboutToDisappear() { this.httpService.destroy(); this.historyDb.close(); } async loadHistory() { this.loading = true; this.errorMsg = ''; try { // 先查本地 SQLite(快) const local = await this.historyDb.queryRecent(50); if (local.length > 0) { this.history = local; } // 异步拉服务器同步(第 67 篇会专讲同步策略) this.syncFromServer(); } catch (error) { this.errorMsg = (error as Error).message; } finally { this.loading = false; } } async syncFromServer() { try { const remote = await this.httpService.fetchHistory(50); // 事务批量插入服务器数据到本地 await this.historyDb.batchInsert(remote.map(r => ({ score: r.score, duration: r.duration, maxCombo: r.maxCombo, mergeCount: r.mergeCount, highestLevel: r.highestLevel, date: r.date }))); // 重新查本地 this.history = await this.historyDb.queryRecent(50); } catch (error) { console.error(`服务器同步失败: ${(error as Error).message}`); } } build() { Column() { Row() { Text('📚 战绩历史').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2C3E50') } .width('100%').height(56).padding({ left: 16 }) .justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Center) if (this.loading) { Column() { LoadingProgress().width(48).height(48).color('#3498DB') Text('加载中...').fontSize(16).fontColor('#95A5A6').margin({ top: 12 }) } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) } else if (this.history.length === 0) { Column() { Text('📭').fontSize(48).margin({ bottom: 12 }) Text('暂无战绩').fontSize(16).fontColor('#95A5A6') } .width('100%').layoutWeight(1) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) } else { List() { ForEach(this.history, (record: GameRecord) => { ListItem() { Row() { Column() { Text('得分').fontSize(10).fontColor('#95A5A6') Text(record.score.toString()) .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2ECC71') } .alignItems(HorizontalAlign.Start).layoutWeight(2) Column() { Text('用时').fontSize(10).fontColor('#95A5A6') Text(this.formatTime(record.duration)) .fontSize(16).fontColor('#2C3E50') } .alignItems(HorizontalAlign.Center).layoutWeight(2) Column() { Text('连击').fontSize(10).fontColor('#95A5A6') Text(`x${record.maxCombo}`) .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E74C3C') } .alignItems(HorizontalAlign.End).layoutWeight(2) } .width('100%').height(64) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .onClick(() => { // 长按删除(第 51 篇 onTouch 思路) this.showDeleteConfirm(record.id); }) } }, (record: GameRecord) => `history_${record.id}`) } .width('100%').layoutWeight(1) .divider({ strokeWidth: 1, color: '#ECF0F1', startMargin: 16, endMargin: 16 }) } } .width('100%').height('100%') .backgroundColor('#FFFFFF') } formatTime(seconds: number): string { const min = Math.floor(seconds / 60); const sec = seconds % 60; return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; } showDeleteConfirm(id: number) { // 简化:用 AlertDialog 或自定义确认弹窗 console.info(`确认删除战绩 id=${id}`); } }六、SQLite vs Preference 取舍
6.1 对比
| 维度 | SQLite(relationalStore) | Preference(kvStore) |
|---|---|---|
| 数据类型 | 表(行×列) | 键值(key→value) |
| 适合 | 结构化、列表、关系 | 少量配置、标志位 |
| 查询 | SQL(条件/排序/聚合) | 按 key 取 |
| 体积 | 中(建表开销) | 小 |
| 性能 | 中(SQL 解析) | 高(直接取) |
| 事务 | ✅ 支持 | ❌ 无 |
6.2 「猫猫大作战」取舍
| 数据 | 推荐 | 原因 |
|---|---|---|
| 战绩历史(50+ 条) | SQLite | 列表结构,要排序查询 |
| 玩家昵称 | Preference | 单字符串 |
| 最高分 | Preference | 单数字 |
| 音效开关 | Preference | 单布尔 |
| 皮肤解锁列表 | SQLite | 多条目,要查询 |
| 鉴权 token | Preference | 单字符串 |
| 最近一次登录时间 | Preference | 单数字 |
关键经验:「列表/关系/要查询」用 SQLite,「单值/标志/配置」用 Preference——本系列第 65 篇会专讲 Preference。
七、踩坑提示
7.1 忘初始化 store 就用
// ❌ 错误:没 init 就 insert,store 是 null async insert(record) { await this.store!.insert(/* ... */); // store 是 null,抛错 } // ✅ 正确:aboutToAppear 先 init aboutToAppear() { this.historyDb.init(this.context); } // init 完成后才 insert7.2 SQL 注入(拼接字符串)
// ❌ 错误:拼接字符串,注入风险 const sql = `SELECT * FROM history WHERE date = '${userInput}'`; // userInput = ' OR 1=1 --,拉出全部数据 // ✅ 正确:参数化查询 const sql = `SELECT * FROM history WHERE date = ?`; await store.querySql(sql, [userInput]); // 或用 RdbPredicates const predicates = new RdbPredicates('history'); predicates.equalTo('date', userInput);7.3 ResultSet 忘遍历
// ❌ 错误:query 返回 ResultSet,没遍历就当数组用 const result = await store.query(predicates); console.info(result.length); // undefined,ResultSet 没 length // ✅ 正确:goToNextRow 遍历 const records = []; while (result.goToNextRow()) { records.push(/* ... */); }7.4 事务忘回滚
// ❌ 错误:事务失败没 rollBack,部分数据已插入 await store.beginTransaction(); for (const record of records) { await store.insert(/* ... */); // 第 50 条失败抛错 } // 忘了 catch 里 rollBack,前 49 条已插入但数据不完整 await store.commit(); // ✅ 正确:try-catch + rollBack await store.beginTransaction(); try { for (const record of records) { await store.insert(/* ... */); } await store.commit(); } catch (error) { await store.rollBack(); // 失败回滚 throw error; }7.5 忘 close 泄漏
// ❌ 错误:组件销毁没 close,连接泄漏 aboutToDisappear() { /* 忘了 this.historyDb.close(); */ } // ✅ 正确:销毁时关连接 aboutToDisappear() { this.historyDb.close(); }7.6 主键自增理解错
// ❌ 错误:插入时手动设 id,与 AUTOINCREMENT 冲突 await store.insert('history', { 'id': 100, 'score': 1500, /* ... */ }); // ✅ 正确:不设 id,让数据库自增 await store.insert('history', { 'score': 1500, /* ... */ }); // 数据库自动分配 id = 1, 2, 3, ...八、调试技巧
console.info打执行的 SQL:追查询/插入是否正确。- DevEco Database Inspector:查看数据库文件、表结构、数据,类似 Android Database Inspector。
- 查询返回空排查:检查表名是否对;检查 WHERE 条件是否过严;检查表是否真有数据。
- 插入失败排查:检查必填字段(NOT NULL)是否都传;检查类型是否匹配(INTEGER 传字符串会报错)。
九、性能与最佳实践
- 结构化/列表/要查询用 SQLite,单值/配置用 Preference——按数据类型选。
- 建表用 IF NOT EXISTS——重启幂等,不报错。
- 查询用参数化(querySql + ?)或 RdbPredicates——防 SQL 注入。
- 批量插入用事务——性能提升 40 倍,且保证原子性。
- 事务失败必 rollBack——否则数据不一致。
- aboutToAppear init,aboutToDisappear close——避免连接泄漏。
- 不手动设主键——让 AUTOINCREMENT 自增。
- 安全等级选对——普通数据 S2,公开缓存 S1,敏感 S3/S4。
十、阶段四进度(61–65)
本篇是阶段四「网络与数据」第 4 篇:
| 篇 | 主题 | 核心要点 |
|---|---|---|
| 61 | HTTP 请求 | http 模块、GET/POST、错误处理 |
| 62 | REST/GraphQL | 接口设计取舍 |
| 63 | 数据序列化 | JSON stringify/parse、Protobuf |
| 64(本篇) | SQLite 持久化 | relationalStore 建表、CRUD、事务 |
| 65 | Preference 键值 | 轻量配置存储 |
总结
本篇我们从 SQLite 持久化切入,掌握了relationalStore 建库建表(StoreConfig + CREATE TABLE)、增删改查(insert/querySql/update/delete + RdbPredicates 防注入)、事务批量插入(beginTransaction + commit + rollBack)、**与 Preference 取舍(列表用 SQLite,单值用 Preference)**四大要点,并给出了 HistoryDb 完整实现 + 战绩页改造代码。核心要点:建表 IF NOT EXISTS 幂等;查询参数化防注入;批量用事务提升 40 倍;aboutToAppear init aboutToDisappear close;列表用 SQLite 单值用 Preference。
下一篇我们将拆解 Preference——键值存储轻量配置。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/services/HistoryDb.ets、entry/src/main/ets/pages/Index.ets、entry/src/main/ets/pages/LeaderboardPage.ets - @ohos.data.relationalStore 官方指南
- RdbPredicates API 官方文档
- HarmonyOS 数据存储最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md