HarmonyOS应用开发实战:猫猫大作战-relationalStore 建库建表、增删改查 SQL、事务批量插入、与 Preference 键值取 前言前面我们用 HTTP 拉排行榜、上报得分——但应用关闭后本地数据全没了下次开要重新拉。战绩历史、玩家昵称、皮肤解锁这种结构化持久数据要存本地数据库。HarmonyOS 提供ohos.data.relationalStoreSQLite 封装做关系型存储——建表、插入、查询、更新、删除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): Promisevoid { const config: relationalStore.StoreConfig { name: this.dbName, securityLevel: relationalStore.SecurityLevel.S1 }; this.store await relationalStore.getRdbStore(context, config); await this.createTable(); } private async createTable(): Promisevoid { 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: OmitGameRecord, id): Promisenumber { 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): PromiseGameRecord[] { 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): Promisevoid { 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 创建 RdbStoreimport { 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数据库文件名.dbapp.dbsecurityLevel安全等级S1–S4S1最低普通数据安全等级等级含义适合S1开放低公开数据排行榜缓存S2普通用户数据战绩、昵称S3个人敏感身份信息S4严格机密密码、财务关键经验「战绩/昵称」用 S2「公开缓存」用 S1——不要全部 S4否则备份/迁移受限。2.3 建表 SQLconst 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文本日期、昵称、UUIDREAL浮点评分、坐标BLOB二进制头像、序列化数据三、增删改查 SQL3.1 增insert// 方式 AAPI 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// 方式 ARdbPredicates推荐 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// 方式 ARdbPredicates推荐 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 事务 APIawait 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.etsimport { 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): Promisevoid { const config: relationalStore.StoreConfig { name: this.dbName, securityLevel: relationalStore.SecurityLevel.S2 }; this.store await relationalStore.getRdbStore(context, config); await this.createTable(); } private async createTable(): Promisevoid { 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: OmitGameRecord, id): Promisenumber { 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: OmitGameRecord, id[]): Promisevoid { 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): PromiseGameRecord[] { 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(): Promisenumber { 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(): Promisenumber { 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): PromiseGameRecord[] { 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: PartialGameRecord): Promisenumber { 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): Promisenumber { const predicates new relationalStore.RdbPredicates(this.tableName); predicates.equalTo(id, id); return await this.store!.delete(predicates); } // 删除所有清空表 async deleteAll(): Promisenumber { const predicates new relationalStore.RdbPredicates(this.tableName); return await this.store!.delete(predicates); } // 删除 30 天前的旧记录清理 async deleteOlderThan(days: number): Promisenumber { 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(): Promisevoid { if (this.store) { this.store null; // 简化实际可能有 release/close API } } }5.2 Index 结束游戏时插入战绩// 来源entry/src/main/ets/pages/Index.ets endGameSQLite 改造后 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.etsSQLite 改造后 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 对比维度SQLiterelationalStorePreferencekvStore数据类型表行×列键值key→value适合结构化、列表、关系少量配置、标志位查询SQL条件/排序/聚合按 key 取体积中建表开销小性能中SQL 解析高直接取事务✅ 支持❌ 无6.2 「猫猫大作战」取舍数据推荐原因战绩历史50 条SQLite列表结构要排序查询玩家昵称Preference单字符串最高分Preference单数字音效开关Preference单布尔皮肤解锁列表SQLite多条目要查询鉴权 tokenPreference单字符串最近一次登录时间Preference单数字关键经验「列表/关系/要查询」用 SQLite「单值/标志/配置」用 Preference——本系列第 65 篇会专讲 Preference。七、踩坑提示7.1 忘初始化 store 就用// ❌ 错误没 init 就 insertstore 是 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 11 --拉出全部数据 // ✅ 正确参数化查询 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); // undefinedResultSet 没 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 initaboutToDisappear close——避免连接泄漏。不手动设主键——让 AUTOINCREMENT 自增。安全等级选对——普通数据 S2公开缓存 S1敏感 S3/S4。十、阶段四进度61–65本篇是阶段四「网络与数据」第 4 篇篇主题核心要点61HTTP 请求http 模块、GET/POST、错误处理62REST/GraphQL接口设计取舍63数据序列化JSON stringify/parse、Protobuf64本篇SQLite 持久化relationalStore 建表、CRUD、事务65Preference 键值轻量配置存储总结本篇我们从 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.etsohos.data.relationalStore 官方指南RdbPredicates API 官方文档HarmonyOS 数据存储最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md