文章配图: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'))
    };
  }
}

核心问题

  1. relationalStore 怎么建库建表?
  2. 增删改查四种 SQL 怎么用?参数化查询怎么防注入?
  3. 事务怎么批量插入?
  4. 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, ...

八、调试技巧

  1. console.info 打执行的 SQL:追查询/插入是否正确。
  2. DevEco Database Inspector:查看数据库文件、表结构、数据,类似 Android Database Inspector。
  3. 查询返回空排查:检查表名是否对;检查 WHERE 条件是否过严;检查表是否真有数据。
  4. 插入失败排查:检查必填字段(NOT NULL)是否都传;检查类型是否匹配(INTEGER 传字符串会报错)。

九、性能与最佳实践

  1. 结构化/列表/要查询用 SQLite,单值/配置用 Preference——按数据类型选。
  2. 建表用 IF NOT EXISTS——重启幂等,不报错。
  3. 查询用参数化(querySql + ?)或 RdbPredicates——防 SQL 注入。
  4. 批量插入用事务——性能提升 40 倍,且保证原子性。
  5. 事务失败必 rollBack——否则数据不一致。
  6. aboutToAppear init,aboutToDisappear close——避免连接泄漏。
  7. 不手动设主键——让 AUTOINCREMENT 自增。
  8. 安全等级选对——普通数据 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——键值存储轻量配置。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐