HarmonyOS NEXT におけるノートアプリ実装 - RDBデータ永続化層

ノートアプリの主要機能として以下が含まれる:

  • ノート作成(タイトル・本文・作成日時・更新日時を記録)
  • ノート編集
  • ノート削除
  • 分類管理(カスタムカテゴリの定義・適用)
  • 検索機能(タイトルまたは本文からの部分一致検索)

データストレージ選択

本実装では HarmonyOS の関係型データベース(RDB)を採用する。これは SQLite ベースのローカル DB 実装で、CRUD 操作およびカスタム SQL 実行を許容する。KeyFeatures として:

  • REMOTE_DATA 関数によるセーフガイド付きデータ操作(安全な扱いが可能な Sendable オブジェクト)
  • 並列処理との連携可能な ResultSet 行取得(getSendableRow
  • 2MB 以下のレコードサイズ推奨(上限超えると書き込み成功・読み込み失敗リスクあり)

大規模データ取り扱い時の注意点:

  • 1回のクエリは5000件以内に限定
  • バックグラウンドタスクプールでの実行
  • SQL文字列は簡潔に(JOINstructure・サブクエリを最小化)
  • ページングクエリによるステップ処理推奨

RDB 初期化

HarmonyOS における RdbStore は、データベースのライフサイクル管理・操作を司る中心 API。以下を実行する:

構成定義(src/main/ets/db/index.ets)

import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';

interface StoreConfig {
  name: string;
  securityLevel: number;
}

const dbConfig: StoreConfig = {
  name: 'noteApp.db',
  securityLevel: relationalStore.SecurityLevel.S1
};

let rdbInstance: relationalStore.RdbStore;

const classifySchema = `CREATE TABLE IF NOT EXISTS Category (
  ID INTEGER PRIMARY KEY,
  TITLE TEXT NOT NULL,
  CREATED DATE NOT NULL,
  MODIFIED DATE NOT NULL,
  DISPLAY INTEGER NOT NULL DEFAULT 1
)`;

const noteSchema = `CREATE TABLE IF NOT EXISTS Memo (
  ID INTEGER PRIMARY KEY,
  TITLE TEXT NOT NULL,
  BODY TEXT NOT NULL,
  CATEGORY_ID INTEGER NOT NULL,
  CREATED DATE NOT NULL,
  MODIFIED DATE NOT NULL,
  DISPLAY INTEGER NOT NULL DEFAULT 1
)`;

export const initializeDatabase = async (context: common.UIAbilityContext): Promise<void> => {
  try {
    const store = await relationalStore.getRdbStore(context, dbConfig);
    if (!store) throw new Error("Failed to initialize RDB");

    rdbInstance = store;
    await store.executeSql(classifySchema);
    await store.executeSql(noteSchema);
    console.log('Database ready successfully');
  } catch (e) {
    console.error('DB init failed', e);
  }
};

export const getRdb = (): relationalStore.RdbStore => rdbInstance;

エントリポイントでの起動処理(src/main/ets/entryability/EntryAbility.ets)

async onWindowStageCreate(windowStage: window.WindowStage) {
  hilog.info(0x0000, 'NoteApp', 'onWindowStageCreate started');

  await initializeDatabase(this.context);
  
  windowStage.loadContent('pages/MainActivity', (error) => {
    if (error?.code) {
      hilog.error(0x0000, 'NoteApp', `Failed to load UI: ${JSON.stringify(error)}`);
      return;
    }
    hilog.info(0x0000, 'NoteApp', 'UI loaded');
  });
}

型定義とオブジェクト構造

src/main/ets/types/index.ets に TypeScript インターフェースを定義:

export interface Category {
  id?: number;
  title: string;
  created?: number;
  modified?: number;
  display?: boolean;
}

export interface Memo {
  id?: number;
  title: string;
  body: string;
  categoryId: number;
  created?: number;
  modified?: number;
  display?: boolean;
}

データアクセス層:モデル定義

各操作は専用クラスに抽象化。RdbPredicates を用いて条件付算術を記述:

分類(Category) 操作(src/main/ets/model/Category.ts)

import { getRdb } from '../db/index';
import relationalStore from '@ohos.data.relationalStore';
import { Category } from '../types';

export class CategoryManager {
  private readonly table = 'Category';
  private columns = ['ID', 'TITLE', 'MODIFIED'];

  private getPredicates(): relationalStore.RdbPredicates {
    return new relationalStore.RdbPredicates(this.table);
  }

  async fetchAll(): Promise {
    const predicates = this.getPredicates();
    predicates.orderByDesc('MODIFIED');
    const resultSet = await getRdb().query(predicates, this.columns);
    const result: Category[] = [];

    while (!resultSet.isAtLastRow) {
      resultSet.goToNextRow();
      const id = resultSet.getLong(resultSet.getColumnIndex('ID'));
      const title = resultSet.getString(resultSet.getColumnIndex('TITLE'));
      const modified = resultSet.getLong(resultSet.getColumnIndex('MODIFIED'));
      result.push({ id, title, modified });
    }
    resultSet.close();
    return result;
  }

  async hasDuplicate(title: string): Promise<boolean> {
    const predicates = this.getPredicates();
    predicates.equalTo('TITLE', title);
    const resultSet = await getRdb().query(predicates, ['ID', 'TITLE']);
    const exists = resultSet.rowCount > 0;
    resultSet.close();
    return exists;
  }

  async create(title: string): Promise<void> {
    const now = Date.now();
    await getRdb().insert(this.table, {
      TITLE: title,
      CREATED: now,
      MODIFIED: now,
      DISPLAY: 1
    });
  }

  async update(id: number, newTitle: string): Promise<void> {
    const predicates = this.getPredicates();
    predicates.equalTo('ID', id);
    await getRdb().update({
      TITLE: newTitle,
      MODIFIED: Date.now()
    }, predicates);
  }

  async remove(id: number): Promise<void> {
    const predicates = this.getPredicates();
    predicates.equalTo('ID', id);
    await getRdb().delete(predicates);
  }
}

export const categoryService = new CategoryManager();

メモ(Memo) 操作(src/main/ets/model/Memo.ts)

import { getRdb } from '../db/index';
import relationalStore from '@ohos.data.relationalStore';
import { Memo } from '../types';

export class MemoManager {
  private readonly table = 'Memo';
  private baseColumns = ['ID', 'TITLE', 'BODY', 'CATEGORY_ID', 'CREATED', 'MODIFIED'];

  private createPredicates(): relationalStore.RdbPredicates {
    return new relationalStore.RdbPredicates(this.table);
  }

  async listFor(categoryId: number = -1): Promise {
    const predicates = this.createPredicates();
    predicates.orderByDesc('MODIFIED');

    if (categoryId > 0) {
      predicates.equalTo('CATEGORY_ID', categoryId);
    }

    const result = await getRdb().query(predicates, this.baseColumns);
    const memoList: Memo[] = [];

    while (!result.isAtLastRow) {
      result.goToNextRow();
      const id = result.getLong(result.getColumnIndex('ID'));
      const title = result.getString(result.getColumnIndex('TITLE'));
      const content = result.getString(result.getColumnIndex('BODY'));
      const catId = result.getLong(result.getColumnIndex('CATEGORY_ID'));
      const created = result.getLong(result.getColumnIndex('CREATED'));
      const modified = result.getLong(result.getColumnIndex('MODIFIED'));

      memoList.push({ id, title, body: content, categoryId: catId, created, modified });
    }
    result.close();
    return memoList;
  }

  async insert(memo: Memo): Promise<void> {
    const now = Date.now();
    const record = {
      TITLE: memo.title,
      BODY: memo.body,
      CATEGORY_ID: memo.categoryId,
      CREATED: memo.created || now,
      MODIFIED: memo.modified || now,
      DISPLAY: 1
    };
    await getRdb().insert(this.table, record);
  }

  async edit(memo: Memo): Promise<void> {
    const predicates = this.createPredicates();
    predicates.equalTo('ID', memo.id);
    await getRdb().update({
      TITLE: memo.title,
      BODY: memo.body,
      MODIFIED: Date.now()
    }, predicates);
  }

  async remove(id: number): Promise<void> {
    const predicates = this.createPredicates();
    predicates.equalTo('ID', id);
    await getRdb().delete(predicates);
  }

  async searchBy(keyword: string): Promise {
    const predicates = this.createPredicates();
    predicates
      .like('TITLE', `%${keyword}%`)
      .or()
      .like('BODY', `%${keyword}%`);
    predicates.orderByDesc('MODIFIED');

    const result = await getRdb().query(predicates, this.baseColumns);
    const matched: Memo[] = [];

    while (!result.isAtLastRow) {
      result.goToNextRow();
      const id = result.getLong(result.getColumnIndex('ID'));
      const title = result.getString(result.getColumnIndex('TITLE'));
      const content = result.getString(result.getColumnIndex('BODY'));
      const catId = result.getLong(result.getColumnIndex('CATEGORY_ID'));
      const created = result.getLong(result.getColumnIndex('CREATED'));
      const modified = result.getLong(result.getColumnIndex('MODIFIED'));

      matched.push({ id, title, body: content, categoryId: catId, created, modified });
    }
    result.close();
    return matched;
  }
}

export const memoService = new MemoManager();

本実装により、ローカル RDB を活用した堅牢なデータライフサイクル管理レイヤーが構築される。

タグ: HarmonyOS RDB SQLite TaskPool RdbPredicates

7月8日 19:53 投稿