HarmonyOS应用开发实战:猫猫大作战-relationalStore 建库建表、增删改查 SQL、事务批量插入、与 Preference 键值取


前言
前面我们用 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 完成后才 insert
7.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
更多推荐



所有评论(0)