ノートアプリの主要機能として以下が含まれる:
- ノート作成(タイトル・本文・作成日時・更新日時を記録)
- ノート編集
- ノート削除
- 分類管理(カスタムカテゴリの定義・適用)
- 検索機能(タイトルまたは本文からの部分一致検索)
データストレージ選択
本実装では 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 を活用した堅牢なデータライフサイクル管理レイヤーが構築される。