データベース
Elftia はローカルデータベースソリューションとして better-sqlite3 + Drizzle ORM を採用しています。すべてのデータベース操作は Worker Thread を通じて非同期に実行されます。
技術選定
| 項目 | 選択 | 理由 |
|---|---|---|
| データベースエンジン | better-sqlite3 | 組み込み型、設定不要、高パフォーマンス |
| ORM | Drizzle ORM | 型安全、ランタイムオーバーヘッドなし、SQL ライクな API |
| モード | WAL (Write-Ahead Logging) | 同時読み取りをサポート;書き込みが読み取りをブロックしない |
| アクセスパターン | Worker Thread RPC | メインスレッドのブロッキングを回避 |
データベースの場所
{userData}/elftia.db # メインデータベースファイル
{userData}/elftia.db-wal # WAL ログ
{userData}/elftia.db-shm # 共有メモリファイル
プラットフォームごとの {userData} パス:
- Windows:
%APPDATA%/elftia/ - macOS:
~/Library/Application Support/elftia/ - Linux:
~/.config/elftia/
Worker Thread アーキテクチャ
すべてのデータベース操作は DbClient -> db.worker.ts の RPC パターンを通じて実行されます:
sequenceDiagram
participant S as Service Layer
participant C as DbClient (main thread)
participant W as db.worker.ts (Worker Thread)
participant DB as SQLite (WAL)
Note over S,DB: Initialization
C->>W: Create Worker({dbPath})
W->>DB: new Database(dbPath)
W->>DB: PRAGMA journal_mode = WAL
W->>DB: drizzle(db) + migrate()
W-->>C: ready signal
Note over S,DB: Normal operation
S->>C: db.chatSessionsList()
C->>C: id = ++seq
C->>W: postMessage({id, method: 'chatSessions:list', params})
W->>DB: SELECT * FROM chat_sessions ...
DB-->>W: Result rows
W-->>C: postMessage({id, result: rows})
C->>C: pending[id].resolve(rows)
C-->>S: Promise<Session[]>
DbClient コア実装
{/* packages/desktop/app/main/workers/DbClient.ts */}
class DbClient extends EventEmitter implements DbRpc {
private worker: Worker;
private seq = 0;
private pending = Map<number, { resolve, reject }>;
constructor(dbPath: string, options?: DbClientOptions) {
this.worker = new Worker(workerPath, {
workerData: { dbPath, diagnosticsDir },
});
this.worker.on('message', (msg) => this.handleMessage(msg));
}
async ready(): Promise<void>;
private async send(method: string, params: unknown): Promise<unknown> {
const id = ++this.seq;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, method, params });
});
}
// 100+ typed methods
chatSessionsList(): Promise<ChatSession[]>;
chatMessagesInsert(msg: ChatMessageInput): Promise<void>;
auditLogInsert(entry: AuditLogEntry): Promise<void>;
}
RPC 型定義
{/* packages/desktop/app/main/workers/types.ts */}
interface DbRequest {
id: number;
method: string;
params: unknown;
}
interface DbResponse {
id: number;
result?: unknown;
error?: { message: string; stack?: string };
}
interface DbRpc {
chatSessionsList(): Promise<ChatSession[]>;
chatSessionsGet(id: string): Promise<ChatSession | null>;
chatSessionsCreate(input: ChatSessionInput): Promise<ChatSession>;
chatSessionsUpdate(id: string, data: Partial<ChatSession>): Promise<void>;
chatSessionsDelete(id: string): Promise<void>;
// ... 100+ methods
}
スキーマ構成
データベーススキーマは Drizzle ORM を使用して定義されており、packages/desktop/app/main/db/schema/ に配置されています:
packages/desktop/app/main/db/schema/
├── index.ts # 統合エクスポート
├── accounts.ts # ユーザーアカウント
├── chat.ts # チャットメッセージ
├── sessions.ts # チャットセッション
├── llm.ts # LLM プロバイダー/モデル/キー
├── projects.ts # プロジェクト
├── settings.ts # アプリケーション設定
├── attachments.ts # 添付ファイル
├── media.ts # メディアリソース
└── templates.ts # テンプレート
コアデータテーブル
チャット関連
{/* テーブル構造定義(簡略版) */}
// チャットセッション
interface ChatSession {
id: string; // UUID
title: string; // セッションタイトル
type: 'chat' | 'roleplay' | 'agent';
agentId?: string; // 関連する Agent ID
personaId?: string; // 関連するペルソナ ID
providerId?: string; // LLM プロバイダー ID
modelId?: string; // モデル ID
rpConfig?: RPConfig; // RP 設定(JSON)
createdAt: number;
updatedAt: number;
lastMessageAt?: number;
messageCount: number;
pinned: boolean;
folderId?: string;
}
// チャットメッセージ
interface ChatMessage {
id: string; // UUID
sessionId: string; // 所属セッション ID
role: 'user' | 'assistant' | 'system';
content: string; // メッセージ内容
parentId?: string; // 親メッセージ ID(ブランチサポート)
reasoning?: string; // 推論内容(思考)
toolCalls?: ToolCall[]; // ツール呼び出し(JSON)
meta?: MessageMeta; // メタデータ(providerId、modelId、tokenCount)
createdAt: number;
}
アカウント関連
// ユーザーアカウント
interface Account {
id: string;
email: string;
displayName?: string;
avatarUrl?: string;
createdAt: number;
}
// アカウントトークン
interface AccountToken {
id: string;
accountId: string;
provider: string; // OAuth プロバイダー
accessToken: string; // 保存時に暗号化
refreshToken?: string;
expiresAt?: number;
}
LLM 設定
// LLM プロバイダー
interface LLMProvider {
id: string;
name: string;
type: string; // openai, anthropic, google など
apiKey?: string; // 保存時に暗号化
baseUrl?: string; // カスタム API エンドポイント
enabled: boolean;
models: string[]; // 利用可能なモデルリスト
}
// API キープール(マルチキーサポート)
interface ApiKeyEntry {
id: string;
providerId: string;
label?: string;
apiKey: string; // 保存時に暗号化
weight: number; // ラウンドロビンの重み
enabled: boolean;
createdAt: number;
}
プロジェクト関連
// プロジェクト
interface Project {
id: string;
name: string;
path: string; // ファイルシステムパス
description?: string;
createdAt: number;
updatedAt: number;
}
セキュリティ監査
// 監査ログエントリ
interface AuditLogEntry {
id: string;
timestamp: number;
eventType: string; // tool_executed、injection_detected など
severity: 'info' | 'warning' | 'critical';
channelId?: string;
userId?: string;
toolName?: string;
details: string; // JSON シリアライズされた詳細
}
メディアとリソース
// メディアリソース
interface MediaResource {
id: string;
sessionId: string;
messageId: string;
filePath: string;
mimeType: string;
thumbnailPath?: string;
size: number;
createdAt: number;
}
// 添付ファイル
interface Attachment {
id: string;
sessionId: string;
messageId: string;
name: string;
type: string;
url?: string;
size: number;
}
その他のコアテーブル
| テーブル | 用途 | スキーマファイル |
|---|---|---|
settings | アプリケーション設定(キーバリュー) | schema/settings.ts |
templates | 画像テンプレート | schema/templates.ts |
channel_instances | チャンネルインスタンス | Worker db/channels.ts |
channel_users | チャンネルユーザー | Worker db/channelUsers.ts |
character_cards | キャラクターカード | Worker db/characterCards.ts |
world_info_books | ワールド情報ブック | Worker db/worldInfo.ts |
world_info_entries | ワールド情報エントリ | Worker db/worldInfo.ts |
custom_agents | カスタム Agent | Worker db/customAgents.ts |
personas | ペルソナ定義 | Worker db/personas.ts |
user_skills | ユーザー Skill | Worker db/userSkills.ts |
tags | タグ | Worker db/tags.ts |
notes | ノートインデックス | Worker db/notes.ts |
note_folders | ノートフォルダ | Worker db/noteFolders.ts |
cron_jobs | スケジュールタスク | CronService ファイルシステム |
DB Worker モジュール
具体的なデータベース操作の実装は packages/desktop/app/main/workers/db/ 配下の複数のモジュールに分散しています:
workers/db/
├── index.ts # メインディスパッチャー;メソッドプレフィックスに基づき適切なモジュールにルーティング
├── chatSessions.ts # chatSessions:* メソッド
├── chatMessages.ts # chatMessages:* メソッド
├── customAgents.ts # customAgents:* メソッド
├── personas.ts # personas:* メソッド
├── characterCards.ts # characterCards:* メソッド
├── worldInfo.ts # worldInfo:* メソッド
├── groupChat.ts # groupChat:* メソッド
├── channels.ts # channels:* メソッド
├── channelUsers.ts # channelUsers:* メソッド
├── auditLog.ts # auditLog:* メソッド
├── apiKeys.ts # apiKeys:* メソッド
├── tags.ts # tags:* メソッド
├── notes.ts # notes:* メソッド
├── noteFolders.ts # noteFolders:* メソッド
├── userSkills.ts # userSkills:* メソッド
├── rpDefaults.ts # rpDefaults:* メソッド
└── characterSprites.ts # sprites:* メソッド
マイグレーションシステム
Drizzle マイグレーション
スキーマの変更は Drizzle の migrate() を通じて自動的に適用されます:
{/* packages/desktop/app/main/db/index.ts */}
function initDatabase() {
const sqlite = new Database(dbPath);
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
const db = drizzle(sqlite);
migrate(db, { migrationsFolder: 'drizzle' });
}
カスタムマイグレーション
複雑なデータ移行ロジックは migrations.ts に実装されています:
{/* カスタムマイグレーションの例 */}
async function migrateApiKeyPool(db: DbRpc) {
const providers = await db.llmProvidersList();
for (const p of providers) {
if (p.apiKey) {
await db.apiKeysInsert({
providerId: p.id,
apiKey: p.apiKey,
weight: 1,
enabled: true,
});
}
}
}
初期化シーケンス
sequenceDiagram
participant M as Main Process
participant C as DbClient
participant W as db.worker.ts
participant D as SQLite
M->>C: new DbClient(dbPath)
C->>W: Create Worker Thread
W->>D: new Database(dbPath)
W->>D: PRAGMA journal_mode = WAL
W->>D: PRAGMA foreign_keys = ON
W->>W: Register all RPC methods
W-->>C: ready signal
M->>C: await db.ready()
Note over M: Worker 初期化完了;続行
M->>M: initDatabase()
Note over M: Drizzle ORM 初期化 + migrate
M->>M: db に依存するサービスを作成
重要: Worker 初期化中の SQLITE_BUSY エラーを防ぐため、他のサービスを作成する前に await db.ready() を完了させる必要があります。
データベース最適化
DatabaseOptimizer サービスは定期的にメンテナンス操作を実行します:
| 操作 | 説明 | 頻度 |
|---|---|---|
PRAGMA optimize | 統計情報を更新 | アプリ終了時 |
VACUUM | 領域を解放 | 手動トリガー |
ANALYZE | クエリプランを更新 | 定期的 |
| WAL チェックポイント | WAL をメインファイルにマージ | SQLite 自動 |
関連ファイル
| ファイル | 説明 |
|---|---|
packages/desktop/app/main/db/index.ts | データベース初期化(WAL + Drizzle + migrate) |
packages/desktop/app/main/db/schema/ | Drizzle ORM スキーマ定義ディレクトリ |
packages/desktop/app/main/workers/DbClient.ts | データベース Worker クライアント |
packages/desktop/app/main/workers/db.worker.ts | データベース Worker 実装 |
packages/desktop/app/main/workers/db/ | ドメイン分割 DB 操作モジュール |
packages/desktop/app/main/workers/types.ts | DbRpc インターフェース定義 |
packages/desktop/app/main/services/persistence/db-optimizer/DatabaseOptimizer.ts | データベース最適化サービス |
packages/desktop/app/main/services/platform/migration/MigrationService.ts | カスタムマイグレーションサービス |