問題描述

在開發鴻蒙原生應用時,如何優雅地實現 RelationalStore 數據庫的初始化、表結構創建、索引優化以及數據庫版本升級?很多開發者在實際項目中會遇到以下問題:

  1. 數據庫初始化流程不清晰
  2. 數據庫版本升級時如何保證數據安全
  3. 如何優化數據庫查詢性能
  4. 如何處理數據庫升級失敗的回滾

本文基於真實的人情管理系統項目,分享一套完整的數據庫管理解決方案。

技術要點

  • RelationalStore 數據庫管理
  • 數據庫版本升級機制
  • 事務處理與回滾
  • 索引優化策略
  • 單例模式應用

完整實現代碼

/**
 * 數據庫管理器
 * 負責數據庫的創建、升級和數據操作
 */

import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

export class DatabaseManager {
  private static instance: DatabaseManager;
  private store: relationalStore.RdbStore | null = null;
  private readonly DB_NAME = 'lelv_human_relations.db';
  private readonly DB_VERSION = 2; // 當前數據庫版本
  private readonly CURRENT_VERSION_KEY = 'database_version';

  private constructor() {}

  /**
   * 獲取單例實例
   */
  public static getInstance(): DatabaseManager {
    if (!DatabaseManager.instance) {
      DatabaseManager.instance = new DatabaseManager();
    }
    return DatabaseManager.instance;
  }

  /**
   * 初始化數據庫
   */
  public async initDatabase(context: common.UIAbilityContext): Promise<void> {
    try {
      console.info('開始初始化數據庫...');
      
      const config: relationalStore.StoreConfig = {
        name: this.DB_NAME,
        securityLevel: relationalStore.SecurityLevel.S1
      };

      // 創建數據庫連接
      this.store = await relationalStore.getRdbStore(context, config);
      console.info('數據庫連接創建成功');
      
      // 創建數據表
      await this.createTables();
      
      // 檢查數據庫版本並執行升級
      await this.checkAndUpgradeDatabase();
      
      console.info('數據庫初始化成功');
    } catch (error) {
      console.error('數據庫初始化失敗:', JSON.stringify(error));
      throw new Error(`數據庫初始化失敗: ${error.message}`);
    }
  }

  /**
   * 創建數據表
   */
  private async createTables(): Promise<void> {
    if (!this.store) {
      throw new Error('數據庫未初始化');
    }

    // 創建人物表
    const createPersonTable = `
      CREATE TABLE IF NOT EXISTS lelv_persons (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        relationship_type TEXT NOT NULL,
        relationship_tags TEXT,
        phone TEXT,
        avatar TEXT,
        contact_id TEXT,
        create_time INTEGER NOT NULL,
        update_time INTEGER NOT NULL
      )
    `;

    // 創建人情記錄表
    const createRecordTable = `
      CREATE TABLE IF NOT EXISTS lelv_human_records (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        event_type TEXT NOT NULL,
        custom_event_type TEXT,
        amount REAL NOT NULL,
        event_time INTEGER NOT NULL,
        person_id TEXT NOT NULL,
        location TEXT,
        remark TEXT,
        photos TEXT,
        custom_fields TEXT,
        create_time INTEGER NOT NULL,
        update_time INTEGER NOT NULL,
        FOREIGN KEY (person_id) REFERENCES lelv_persons (id)
      )
    `;

    // 創建應用設置表
    const createSettingsTable = `
      CREATE TABLE IF NOT EXISTS lelv_app_settings (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL,
        update_time INTEGER NOT NULL
      )
    `;

    await this.store.executeSql(createPersonTable);
    await this.store.executeSql(createRecordTable);
    await this.store.executeSql(createSettingsTable);

    // 創建索引以提升查詢性能
    await this.createIndexes();
  }

  /**
   * 創建索引
   */
  private async createIndexes(): Promise<void> {
    if (!this.store) return;

    const indexes = [
      'CREATE INDEX IF NOT EXISTS idx_records_person_id ON lelv_human_records (person_id)',
      'CREATE INDEX IF NOT EXISTS idx_records_event_time ON lelv_human_records (event_time)',
      'CREATE INDEX IF NOT EXISTS idx_records_type ON lelv_human_records (type)',
      'CREATE INDEX IF NOT EXISTS idx_persons_name ON lelv_persons (name)',
      'CREATE INDEX IF NOT EXISTS idx_persons_phone ON lelv_persons (phone)'
    ];

    for (const index of indexes) {
      await this.store.executeSql(index);
    }
  }

  /**
   * 檢查並升級數據庫
   */
  private async checkAndUpgradeDatabase(): Promise<void> {
    if (!this.store) return;

    try {
      // 獲取當前數據庫版本
      const currentVersion = await this.getCurrentVersion();
      console.info(`當前數據庫版本: ${currentVersion}, 目標版本: ${this.DB_VERSION}`);

      if (currentVersion < this.DB_VERSION) {
        console.info('開始數據庫升級...');
        await this.upgradeDatabase(currentVersion, this.DB_VERSION);
        await this.setCurrentVersion(this.DB_VERSION);
        console.info('數據庫升級完成');
      }
    } catch (error) {
      console.error('數據庫升級失敗:', JSON.stringify(error));
      throw new Error('數據庫升級失敗');
    }
  }

  /**
   * 獲取當前數據庫版本
   */
  private async getCurrentVersion(): Promise<number> {
    if (!this.store) return 0;

    try {
      const resultSet = await this.store.querySql(
        'SELECT value FROM lelv_app_settings WHERE key = ?',
        [this.CURRENT_VERSION_KEY]
      );

      if (resultSet.goToFirstRow()) {
        const versionIndex = resultSet.getColumnIndex('value');
        if (versionIndex >= 0) {
          const version = resultSet.getLong(versionIndex);
          resultSet.close();
          return version;
        }
      }
      resultSet.close();
      return 0; // 默認版本0
    } catch (error) {
      console.warn('獲取數據庫版本失敗,使用默認版本0');
      return 0;
    }
  }

  /**
   * 設置當前數據庫版本
   */
  private async setCurrentVersion(version: number): Promise<void> {
    if (!this.store) return;

    const now = Date.now();
    await this.store.executeSql(
      'INSERT OR REPLACE INTO lelv_app_settings (key, value, update_time) VALUES (?, ?, ?)',
      [this.CURRENT_VERSION_KEY, version.toString(), now]
    );
  }

  /**
   * 升級數據庫
   */
  private async upgradeDatabase(fromVersion: number, toVersion: number): Promise<void> {
    if (!this.store) return;

    try {
      // 開啓事務
      await this.store.beginTransaction();

      // 從版本1升級到版本2
      if (fromVersion < 2 && toVersion >= 2) {
        await this.upgradeToVersion2();
      }

      // 可以繼續添加更多版本的升級邏輯
      // if (fromVersion < 3 && toVersion >= 3) {
      //   await this.upgradeToVersion3();
      // }

      // 提交事務
      await this.store.commit();
    } catch (error) {
      // 回滾事務
      await this.store.rollback(void 0);
      throw new Error(`數據庫升級失敗: ${error.message}`);
    }
  }

  /**
   * 升級到版本2
   */
  private async upgradeToVersion2(): Promise<void> {
    if (!this.store) return;

    console.info('執行數據庫升級到版本2...');

    // 添加新的索引以提升查詢性能
    const newIndexes = [
      'CREATE INDEX IF NOT EXISTS idx_records_amount ON lelv_human_records (amount)',
      'CREATE INDEX IF NOT EXISTS idx_persons_relationship_type ON lelv_persons (relationship_type)'
    ];

    for (const indexSql of newIndexes) {
      await this.store.executeSql(indexSql);
    }

    // 添加新的字段
    try {
      await this.store.executeSql(`
        ALTER TABLE lelv_human_records ADD COLUMN is_deleted INTEGER DEFAULT 0
      `);
    } catch (error) {
      // 字段可能已存在,忽略錯誤
      console.warn('添加is_deleted字段失敗,可能已存在');
    }

    console.info('數據庫升級到版本2完成');
  }

  /**
   * 優化數據庫性能
   */
  public async optimizeDatabase(): Promise<void> {
    if (!this.store) return;

    try {
      console.info('開始數據庫性能優化...');
      
      // 執行VACUUM命令回收空間
      await this.store.executeSql('VACUUM');
      
      // 執行ANALYZE命令更新統計信息
      await this.store.executeSql('ANALYZE');
      
      console.info('數據庫性能優化完成');
    } catch (error) {
      console.error('數據庫性能優化失敗:', JSON.stringify(error));
      throw new Error('數據庫性能優化失敗');
    }
  }

  /**
   * 獲取數據庫實例
   */
  public getStore(): relationalStore.RdbStore | null {
    return this.store;
  }
}

核心原理解析

1. 單例模式設計

使用單例模式確保整個應用只有一個數據庫管理器實例,避免多次初始化造成的資源浪費和數據衝突。

2. 數據庫版本管理

  • 通過 DB_VERSION 常量管理當前數據庫版本
  • lelv_app_settings 表中記錄實際數據庫版本
  • 初始化時自動檢查版本並執行升級

3. 事務處理

數據庫升級使用事務保證原子性:

await this.store.beginTransaction();  // 開啓事務
// ... 執行升級操作
await this.store.commit();             // 提交事務
// 或
await this.store.rollback(void 0);    // 回滾事務

4. 索引優化策略

根據查詢場景創建合適的索引:

  • 主鍵索引: id 字段
  • 外鍵索引: person_id 字段
  • 查詢索引: event_time, type, amount 等常用查詢字段

最佳實踐建議

  1. 數據庫初始化時機​: 在應用啓動時的 UIAbility 中初始化
  2. 版本號管理​: 每次數據庫結構變更都需要增加版本號
  3. 升級腳本測試​: 充分測試各個版本之間的升級路徑
  4. 錯誤處理​: 捕獲異常並提供友好的錯誤提示
  5. 性能監控​: 定期執行 VACUUM 和 ANALYZE 優化數據庫

避坑指南

  1. 不要在主線程執行耗時的數據庫操作
    • 使用 async/await 確保異步執行
  2. 不要忘記關閉 ResultSet
    • 每次查詢後都要調用 resultSet.close()
  3. 不要在事務中執行長時間操作
    • 事務要儘快提交或回滾
  4. ALTER TABLE 時注意兼容性
    • 使用 IF NOT EXISTSDEFAULT

效果展示

數據庫初始化日誌輸出:

開始初始化數據庫...
數據庫連接創建成功
數據表創建完成
當前數據庫版本: 1, 目標版本: 2
開始數據庫升級...
執行數據庫升級到版本2...
數據庫升級到版本2完成
數據庫升級完成
數據庫初始化成功

總結

本方案提供了一套完整的鴻蒙 RelationalStore 數據庫管理解決方案,包括:

  • ✅ 規範的數據庫初始化流程
  • ✅ 安全的版本升級機制
  • ✅ 完善的事務回滾保護
  • ✅ 合理的索引優化策略

相關資源

  • 鴻蒙學習資源