関連資料: CRUD パターン詳解 | localStorage API詳解 | try-catch詳解
データ整合性とは、アプリケーション内のデータが常に正しく一貫した状態を保つことです。不整合データが存在すると、アプリケーションが予期しない動作をしたり、ユーザーに混乱を与えたりします。
// 不整合の例:存在しないタスクを更新しようとする
const tasks = [
{ id: 1, text: "買い物", completed: false }
];
// 危険:存在しないIDを更新
updateTask(999, { completed: true }); // エラーが発生する可能性
// 安全:事前に存在確認
if (taskExists(999)) {
updateTask(999, { completed: true });
} else {
console.error("タスクが見つかりません");
}
入力データの検証により、不正なデータの保存を防ぎます。
class TaskValidator {
static validateTask(task) {
const errors = [];
// 必須フィールドの検証
if (!task.text || task.text.trim() === '') {
errors.push('タスクテキストは必須です');
}
// データ型の検証
if (typeof task.completed !== 'boolean') {
errors.push('完了状態は真偽値である必要があります');
}
// 値の範囲検証
if (task.text && task.text.length > 500) {
errors.push('タスクテキストは500文字以内である必要があります');
}
// 優先度の検証
const validPriorities = ['low', 'normal', 'high'];
if (task.priority && !validPriorities.includes(task.priority)) {
errors.push('無効な優先度です');
}
// 日付の検証
if (task.dueDate && isNaN(new Date(task.dueDate).getTime())) {
errors.push('無効な期限日です');
}
return {
isValid: errors.length === 0,
errors: errors
};
}
static validateTaskList(tasks) {
// 配列の検証
if (!Array.isArray(tasks)) {
return { isValid: false, errors: ['タスクリストは配列である必要があります'] };
}
const errors = [];
const ids = new Set();
tasks.forEach((task, index) => {
// 各タスクの個別検証
const taskValidation = this.validateTask(task);
if (!taskValidation.isValid) {
errors.push(`タスク${index + 1}: ${taskValidation.errors.join(', ')}`);
}
// ID重複の検証
if (task.id) {
if (ids.has(task.id)) {
errors.push(`重複するID: ${task.id}`);
}
ids.add(task.id);
}
});
return {
isValid: errors.length === 0,
errors: errors
};
}
}
不正な状態変更を防ぐための制御機構です。
class TaskStateManager {
constructor() {
// 有効な状態遷移の定義
this.allowedTransitions = {
'draft': ['active', 'cancelled'],
'active': ['completed', 'cancelled', 'paused'],
'paused': ['active', 'cancelled'],
'completed': ['active'], // 完了を取り消すことは可能
'cancelled': ['draft'] // 取り消しから下書きに戻すことは可能
};
}
canTransition(fromState, toState) {
const allowed = this.allowedTransitions[fromState] || [];
return allowed.includes(toState);
}
validateStateTransition(task, newState) {
const currentState = task.status || 'draft';
if (!this.canTransition(currentState, newState)) {
throw new Error(
`無効な状態遷移: ${currentState} から ${newState} への変更はできません`
);
}
// 特定の状態変更に対する追加検証
if (newState === 'completed' && !task.text.trim()) {
throw new Error('空のタスクは完了できません');
}
if (newState === 'active' && task.dueDate && new Date(task.dueDate) < new Date()) {
console.warn('期限切れのタスクをアクティブにしています');
}
return true;
}
}
複数のデータソース間での整合性を保ちます。
class DataSyncManager {
constructor(storageKey) {
this.storageKey = storageKey;
this.memoryCache = null;
this.lastSyncTime = null;
this.version = 0;
}
// メモリとlocalStorageの同期
syncWithStorage() {
try {
const stored = localStorage.getItem(this.storageKey);
const storedData = stored ? JSON.parse(stored) : [];
// バージョン管理による競合検出
const storedVersion = localStorage.getItem(`${this.storageKey}_version`) || 0;
if (this.memoryCache && this.version !== parseInt(storedVersion)) {
console.warn('データの競合が検出されました。最新データで更新します。');
this.handleConflict(this.memoryCache, storedData);
}
this.memoryCache = storedData;
this.version = parseInt(storedVersion);
this.lastSyncTime = new Date();
return storedData;
} catch (error) {
console.error('データ同期エラー:', error);
// エラー時はメモリキャッシュを優先
return this.memoryCache || [];
}
}
// データの保存とバージョン更新
saveToStorage(data) {
try {
// 保存前の整合性チェック
const validation = TaskValidator.validateTaskList(data);
if (!validation.isValid) {
throw new Error(`データ整合性エラー: ${validation.errors.join(', ')}`);
}
// アトミックな保存(バージョンとデータを同時更新)
this.version++;
localStorage.setItem(this.storageKey, JSON.stringify(data));
localStorage.setItem(`${this.storageKey}_version`, this.version.toString());
this.memoryCache = data;
this.lastSyncTime = new Date();
// 他のタブに変更を通知
this.notifyOtherTabs(data);
} catch (error) {
console.error('データ保存エラー:', error);
throw error;
}
}
// 競合解決
handleConflict(localData, storageData) {
// 簡単な競合解決:最新のタイムスタンプを優先
const mergedData = this.mergeDataByTimestamp(localData, storageData);
this.memoryCache = mergedData;
return mergedData;
}
mergeDataByTimestamp(data1, data2) {
const merged = new Map();
// data1のタスクを追加
data1.forEach(task => {
merged.set(task.id, task);
});
// data2のタスクで新しいものがあれば更新
data2.forEach(task => {
const existing = merged.get(task.id);
if (!existing || new Date(task.updatedAt) > new Date(existing.updatedAt)) {
merged.set(task.id, task);
}
});
return Array.from(merged.values());
}
// 他のタブへの通知
notifyOtherTabs(data) {
// Custom event for same-origin tabs
const event = new CustomEvent('dataSync', {
detail: { data, version: this.version, timestamp: new Date().toISOString() }
});
window.dispatchEvent(event);
// BroadcastChannel API for modern browsers
if (typeof BroadcastChannel !== 'undefined') {
const channel = new BroadcastChannel(`${this.storageKey}_sync`);
channel.postMessage({
type: 'dataUpdate',
data: data,
version: this.version,
timestamp: new Date().toISOString()
});
}
}
}
複数の操作を原子的に実行し、失敗時には全体をロールバックします。
class TaskTransaction {
constructor(dataManager) {
this.dataManager = dataManager;
this.operations = [];
this.backup = null;
}
begin() {
// 現在の状態をバックアップ
this.backup = JSON.parse(JSON.stringify(this.dataManager.getTasks()));
this.operations = [];
return this;
}
addOperation(operation) {
this.operations.push(operation);
return this;
}
async execute() {
try {
// すべての操作を順次実行
for (const operation of this.operations) {
await operation();
}
// 成功時:変更をコミット
this.commit();
return { success: true };
} catch (error) {
// 失敗時:ロールバック
this.rollback();
throw new Error(`トランザクション失敗: ${error.message}`);
}
}
commit() {
// バックアップを破棄
this.backup = null;
this.operations = [];
}
rollback() {
if (this.backup) {
// データを元の状態に復元
this.dataManager.setTasks(this.backup);
this.backup = null;
}
this.operations = [];
}
}
// 使用例:複数タスクの一括更新
async function bulkUpdateTasks(taskUpdates) {
const transaction = new TaskTransaction(taskManager);
try {
transaction.begin();
// 各更新操作をトランザクションに追加
taskUpdates.forEach(update => {
transaction.addOperation(async () => {
await taskManager.updateTask(update.id, update.changes);
});
});
await transaction.execute();
console.log('一括更新が完了しました');
} catch (error) {
console.error('一括更新に失敗しました:', error.message);
}
}
データベースのACID原則をフロントエンドに適用する方法:
class ACIDCompliantTaskManager {
constructor() {
this.dataManager = new DataSyncManager('tasks');
this.lockManager = new LockManager();
}
// Atomicity(原子性):全部成功か全部失敗
async atomicOperation(operations) {
const transaction = new TaskTransaction(this.dataManager);
return transaction.begin().addOperations(operations).execute();
}
// Consistency(一貫性):データの整合性を保つ
async saveWithConsistencyCheck(tasks) {
const validation = TaskValidator.validateTaskList(tasks);
if (!validation.isValid) {
throw new Error(`一貫性エラー: ${validation.errors.join(', ')}`);
}
// 参照整合性のチェック(例:親子関係のあるタスク)
this.validateReferentialIntegrity(tasks);
return this.dataManager.saveToStorage(tasks);
}
// Isolation(分離性):操作の分離
async isolatedUpdate(taskId, updateFn) {
const lock = await this.lockManager.acquireLock(taskId);
try {
const task = this.dataManager.getTaskById(taskId);
const updatedTask = updateFn(task);
await this.saveWithConsistencyCheck([updatedTask]);
return updatedTask;
} finally {
this.lockManager.releaseLock(taskId, lock);
}
}
// Durability(永続性):データの永続化
async durableSave(tasks) {
// 複数の保存先に書き込み
const promises = [
this.dataManager.saveToStorage(tasks),
this.saveToIndexedDB(tasks),
this.syncToServer(tasks)
];
await Promise.all(promises);
}
}
すべての変更を履歴として記録し、データの完全な追跡を可能にします:
class TaskEventStore {
constructor() {
this.events = this.loadEvents();
this.snapshots = new Map();
}
// イベントの記録
recordEvent(event) {
const eventWithTimestamp = {
...event,
id: this.generateEventId(),
timestamp: new Date().toISOString(),
version: this.events.length + 1
};
this.events.push(eventWithTimestamp);
this.saveEvents();
return eventWithTimestamp;
}
// イベントからタスクリストを再構築
reconstructTaskList(upToVersion = null) {
const targetEvents = upToVersion
? this.events.slice(0, upToVersion)
: this.events;
const tasks = new Map();
targetEvents.forEach(event => {
switch (event.type) {
case 'TASK_CREATED':
tasks.set(event.taskId, {
id: event.taskId,
text: event.text,
completed: false,
createdAt: event.timestamp,
updatedAt: event.timestamp
});
break;
case 'TASK_UPDATED':
const existingTask = tasks.get(event.taskId);
if (existingTask) {
tasks.set(event.taskId, {
...existingTask,
...event.changes,
updatedAt: event.timestamp
});
}
break;
case 'TASK_DELETED':
tasks.delete(event.taskId);
break;
}
});
return Array.from(tasks.values());
}
// スナップショットの作成(パフォーマンス向上)
createSnapshot(version) {
const taskList = this.reconstructTaskList(version);
this.snapshots.set(version, {
tasks: taskList,
timestamp: new Date().toISOString()
});
// 古いスナップショットの削除
this.cleanupOldSnapshots();
}
// 時点復元
restoreToVersion(version) {
const tasks = this.reconstructTaskList(version);
return tasks;
}
}
WebSocketやSSEを使用したリアルタイムデータ同期:
class RealtimeTaskSync {
constructor(taskManager) {
this.taskManager = taskManager;
this.websocket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
this.websocket = new WebSocket('ws://localhost:8080/tasks');
this.websocket.onopen = () => {
console.log('リアルタイム同期が開始されました');
this.reconnectAttempts = 0;
};
this.websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleRemoteUpdate(data);
};
this.websocket.onclose = () => {
console.log('接続が閉じられました');
this.attemptReconnect();
};
this.websocket.onerror = (error) => {
console.error('WebSocket エラー:', error);
};
}
handleRemoteUpdate(data) {
switch (data.type) {
case 'TASK_UPDATED':
// 競合解決
const localTask = this.taskManager.getTaskById(data.task.id);
const resolvedTask = this.resolveConflict(localTask, data.task);
this.taskManager.updateTaskSilently(data.task.id, resolvedTask);
break;
case 'TASK_CREATED':
this.taskManager.addTaskSilently(data.task);
break;
case 'TASK_DELETED':
this.taskManager.deleteTaskSilently(data.task.id);
break;
}
// UI の更新通知
this.notifyUIUpdate(data);
}
resolveConflict(localTask, remoteTask) {
// Last-Write-Wins 戦略
const localTime = new Date(localTask.updatedAt);
const remoteTime = new Date(remoteTask.updatedAt);
return remoteTime > localTime ? remoteTask : localTask;
}
}
class ConsistentTodoApp {
constructor() {
this.validator = new TaskValidator();
this.stateManager = new TaskStateManager();
this.dataSync = new DataSyncManager('todoTasks');
this.eventStore = new TaskEventStore();
this.setupEventListeners();
this.initializeApp();
}
async initializeApp() {
try {
// データの整合性チェックと復旧
await this.validateAndRepairData();
// UI の初期化
await this.loadAndDisplayTasks();
// マルチタブ同期の設定
this.setupMultiTabSync();
} catch (error) {
this.handleInitializationError(error);
}
}
async validateAndRepairData() {
try {
const tasks = this.dataSync.syncWithStorage();
const validation = this.validator.validateTaskList(tasks);
if (!validation.isValid) {
console.warn('データ整合性エラーが検出されました:', validation.errors);
// 自動修復を試行
const repairedTasks = await this.repairData(tasks, validation.errors);
this.dataSync.saveToStorage(repairedTasks);
this.showNotification('データの不整合を自動修復しました', 'warning');
}
} catch (error) {
console.error('データ検証エラー:', error);
// フォールバック:空のタスクリストで開始
this.dataSync.saveToStorage([]);
}
}
async repairData(tasks, errors) {
const repairedTasks = [];
const usedIds = new Set();
tasks.forEach((task, index) => {
try {
// 必須フィールドの修復
const repairedTask = {
id: task.id || `repaired_${Date.now()}_${index}`,
text: task.text || '(修復されたタスク)',
completed: Boolean(task.completed),
priority: ['low', 'normal', 'high'].includes(task.priority) ? task.priority : 'normal',
createdAt: task.createdAt || new Date().toISOString(),
updatedAt: task.updatedAt || new Date().toISOString()
};
// ID重複の修復
if (usedIds.has(repairedTask.id)) {
repairedTask.id = `${repairedTask.id}_dedup_${Date.now()}`;
}
usedIds.add(repairedTask.id);
// 個別検証
const taskValidation = this.validator.validateTask(repairedTask);
if (taskValidation.isValid) {
repairedTasks.push(repairedTask);
}
} catch (error) {
console.warn(`タスク ${index} の修復に失敗:`, error);
}
});
return repairedTasks;
}
async createTask(taskText, priority = 'normal') {
try {
// 事前検証
const newTask = {
id: this.generateId(),
text: taskText.trim(),
priority: priority,
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
const validation = this.validator.validateTask(newTask);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// トランザクション開始
const transaction = new TaskTransaction(this);
transaction.begin();
try {
// データ追加
const currentTasks = this.dataSync.syncWithStorage();
const updatedTasks = [...currentTasks, newTask];
// 整合性チェック
const listValidation = this.validator.validateTaskList(updatedTasks);
if (!listValidation.isValid) {
throw new Error(`リスト整合性エラー: ${listValidation.errors.join(', ')}`);
}
// 保存
this.dataSync.saveToStorage(updatedTasks);
// イベント記録
this.eventStore.recordEvent({
type: 'TASK_CREATED',
taskId: newTask.id,
text: newTask.text,
priority: newTask.priority
});
transaction.commit();
// UI更新
this.addTaskToUI(newTask);
this.showNotification('タスクが追加されました', 'success');
return newTask;
} catch (error) {
transaction.rollback();
throw error;
}
} catch (error) {
console.error('タスク作成エラー:', error);
this.showNotification(error.message, 'error');
throw error;
}
}
async updateTask(taskId, changes) {
try {
const currentTasks = this.dataSync.syncWithStorage();
const taskIndex = currentTasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`タスク ID: ${taskId} が見つかりません`);
}
const currentTask = currentTasks[taskIndex];
// 状態遷移の検証
if (changes.status) {
this.stateManager.validateStateTransition(currentTask, changes.status);
}
// 更新データの作成
const updatedTask = {
...currentTask,
...changes,
updatedAt: new Date().toISOString()
};
// 検証
const validation = this.validator.validateTask(updatedTask);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// 更新の実行
const newTaskList = [...currentTasks];
newTaskList[taskIndex] = updatedTask;
this.dataSync.saveToStorage(newTaskList);
// イベント記録
this.eventStore.recordEvent({
type: 'TASK_UPDATED',
taskId: taskId,
changes: changes,
previousState: currentTask
});
// UI更新
this.updateTaskInUI(taskId, updatedTask);
this.showNotification('タスクが更新されました', 'success');
return updatedTask;
} catch (error) {
console.error('タスク更新エラー:', error);
this.showNotification(error.message, 'error');
throw error;
}
}
async deleteTask(taskId) {
try {
const currentTasks = this.dataSync.syncWithStorage();
const taskIndex = currentTasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`タスク ID: ${taskId} が見つかりません`);
}
const taskToDelete = currentTasks[taskIndex];
const updatedTasks = currentTasks.filter(task => task.id !== taskId);
this.dataSync.saveToStorage(updatedTasks);
// イベント記録
this.eventStore.recordEvent({
type: 'TASK_DELETED',
taskId: taskId,
deletedTask: taskToDelete
});
// UI更新
this.removeTaskFromUI(taskId);
this.showNotification('タスクが削除されました', 'success');
return taskToDelete;
} catch (error) {
console.error('タスク削除エラー:', error);
this.showNotification(error.message, 'error');
throw error;
}
}
setupMultiTabSync() {
// Storage event for cross-tab synchronization
window.addEventListener('storage', (e) => {
if (e.key === 'todoTasks') {
this.handleExternalDataChange(e.newValue);
}
});
// Custom event for same-tab updates
window.addEventListener('dataSync', (e) => {
this.handleDataSyncEvent(e.detail);
});
}
handleExternalDataChange(newData) {
try {
const tasks = newData ? JSON.parse(newData) : [];
const validation = this.validator.validateTaskList(tasks);
if (validation.isValid) {
this.renderTaskList(tasks);
this.showNotification('他のタブでの変更を同期しました', 'info');
} else {
console.warn('他のタブからの不正なデータを検出:', validation.errors);
}
} catch (error) {
console.error('外部データ変更の処理エラー:', error);
}
}
generateId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
showNotification(message, type) {
// 通知システムの実装
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
}
// アプリケーションの初期化
document.addEventListener('DOMContentLoaded', () => {
try {
const app = new ConsistentTodoApp();
// グローバルエラーハンドリング
window.addEventListener('error', (e) => {
console.error('アプリケーションエラー:', e.error);
app.showNotification('予期しないエラーが発生しました', 'error');
});
window.addEventListener('unhandledrejection', (e) => {
console.error('未処理のPromise拒否:', e.reason);
app.showNotification('非同期処理でエラーが発生しました', 'error');
});
} catch (error) {
console.error('アプリケーション初期化エラー:', error);
document.body.innerHTML = '<p>アプリケーションの初期化に失敗しました。ページを再読み込みしてください。</p>';
}
});
class DataIntegrityChecker {
constructor(taskManager) {
this.taskManager = taskManager;
}
performFullCheck() {
const results = {
passed: 0,
failed: 0,
warnings: 0,
details: []
};
try {
// 基本整合性チェック
this.checkBasicIntegrity(results);
// 参照整合性チェック
this.checkReferentialIntegrity(results);
// ビジネスルールチェック
this.checkBusinessRules(results);
// パフォーマンスチェック
this.checkPerformance(results);
} catch (error) {
results.details.push({
type: 'error',
message: `整合性チェック中にエラーが発生: ${error.message}`
});
results.failed++;
}
return results;
}
checkBasicIntegrity(results) {
const tasks = this.taskManager.getTasks();
// ID重複チェック
const ids = tasks.map(task => task.id);
const uniqueIds = new Set(ids);
if (ids.length !== uniqueIds.size) {
results.details.push({
type: 'error',
message: 'ID重複が検出されました'
});
results.failed++;
} else {
results.passed++;
}
// 必須フィールドチェック
tasks.forEach((task, index) => {
if (!task.id || !task.text || task.completed === undefined) {
results.details.push({
type: 'error',
message: `タスク${index + 1}に必須フィールドが不足しています`
});
results.failed++;
} else {
results.passed++;
}
});
}
generateReport(results) {
const report = {
summary: {
total: results.passed + results.failed + results.warnings,
passed: results.passed,
failed: results.failed,
warnings: results.warnings,
success: results.failed === 0
},
details: results.details,
timestamp: new Date().toISOString()
};
console.log('データ整合性レポート:', report);
return report;
}
}
このデータ整合性管理の実装により、堅牢で信頼性の高いTodoListアプリケーションを構築できます。
参考:さらなる詳細は localStorage パフォーマンス、フロントエンド状態管理 をご覧ください。