[JavaScript] IndexedDBをネイティブコードで実装する方法

はじめに

前回のカードゲームアプリで、IndexedDBをライブラリを活用して実装しましたが、今回も音声データなどやや重い情報を扱うので、indexedDBを活用して毎回ダウンロードする負担を減らそうかと考えてます。

それに対して、今回は、ライブラリを使わずネイティブコードで実装してみてはどうか?というGPT5の提案もあり、やるかどうかは分からないですが、学習記事をまとめてもらったので、ほぼ個人用ですが暇な時に読み返す学習ネタ、備忘録メモとして記事にして残しておきます。

わずか2か月前ぐらいだと悲鳴を上げそうなコードですが、毎日毎日、JavaScriptを書き続けてるので、流石にこれだけコードを見てると読めるようにはなってきてますが、全体像を把握するには何度か読み返す必要がありそうです。

過去記事。

1. IndexedDBとは?

IndexedDBは、Webブラウザで利用できる強力なローカルデータベースの一種です。主に、Webアプリケーションがユーザーのローカルにデータを保存し、オフラインでも動作させるために使われます。これにより、アプリケーションはインターネット接続がない状態でも機能を提供できるようになります。

基本的な特徴

IndexedDBは次の特徴を持っており、Web開発におけるデータストレージ手段として非常に有用です。


1.1 非同期で動作

非同期操作とは、データベースに対する要求(データの読み書き)が行われる際に、UIスレッドをブロックせず、バックグラウンドで処理が進行するということです。

  • メリット

    • ユーザーインターフェースの応答性が保たれる。データの読み書き中でもUIがスムーズに動作し、ユーザー体験が損なわれません。
    • 複数のデータ操作を並行して行うことができ、パフォーマンス向上に繋がります。
  • 非同期の流れ

    • IndexedDBの操作は、コールバック関数やPromiseを使って結果を受け取ります。非同期で行うため、同期的なデータ操作(例えば、画面をブロックして処理を待機する)は発生しません。
  • const request = indexedDB.open('myDatabase');
    
    request.onsuccess = function(event) {
      console.log('Database opened successfully');
    };
    
    request.onerror = function(event) {
      console.error('Error opening database');
    };
    

    上記のコードでは、request.onsuccessrequest.onerrorが非同期的に呼ばれるため、UIの動作をブロックすることなくデータベースのオープン処理が進みます。


1.2 キーと値のペアとしてデータを保存

IndexedDBでは、データはキーと値のペアとして格納されます。このデータの保存方式は、基本的にはオブジェクトストアと呼ばれる保存場所に対して、指定されたキーを用いて**値(データ)**を保存します。

  • キー

    • 各データに一意な識別子を付与するためのもので、通常は文字列や数値で指定します。データを読み書きする際に、このキーを使ってアクセスします。
    • オートインクリメント機能を利用すれば、キーを自動で生成することも可能です。
    • 保存されるデータで、任意のデータ型(文字列、オブジェクト、配列など)を保存できます。
    • 複雑なデータ構造(例えば、オブジェクトや配列)もそのまま保存できるので、構造化されたデータを管理するのに適しています。
  • const db = request.result; // 既に開いたデータベース
    const transaction = db.transaction(['myStore'], 'readwrite');
    const store = transaction.objectStore('myStore');
    
    const record = { id: 1, name: 'Alice', age: 30 };
    const request = store.put(record);  // id 1のレコードを保存
    
    request.onsuccess = function() {
      console.log('Record saved!');
    };
    

ここでは、putメソッドを使って、idをキーとしてレコード(オブジェクト)を保存しています。このように、キーと値のペアで管理するため、効率的にデータにアクセスでき、特に検索性能やデータ更新がスムーズに行えます。


1.3 トランザクション制御やインデックス作成が可能

IndexedDBでは、トランザクションを使用して、データの整合性や一貫性を保ちながらデータベース操作を行います。また、データの高速な検索をサポートするために、インデックスを作成することができます。

トランザクション制御

トランザクションは、データベース内の複数の操作を一つの処理単位としてまとめ、すべての操作が成功した場合にのみその変更をデータベースに反映させます。これにより、データの整合性を保つことができます。

  • const transaction = db.transaction(['myStore'], 'readwrite');
    const store = transaction.objectStore('myStore');
    
    const record = { id: 2, name: 'Bob', age: 25 };
    store.put(record); // データを挿入
    
    transaction.oncomplete = function() {
      console.log('Transaction completed successfully!');
    };
    
    transaction.onerror = function() {
      console.error('Transaction failed!');
    };
    

このように、トランザクションを使用することで、一貫した状態でデータ操作ができます。例えば、データベースに複数のデータを一度に保存する際に、一つでも失敗した場合は全ての操作をキャンセルすることができます。

インデックス作成

インデックスを使うことで、特定のデータフィールドで検索速度を向上させることができます。例えば、名前で検索する場合、インデックスを使うことで、全てのデータをスキャンすることなく、素早く該当するレコードを検索できます。

  • const store = db.createObjectStore('myStore', { keyPath: 'id' });
    store.createIndex('nameIndex', 'name', { unique: false }); // nameフィールドにインデックスを作成
    

ここで、nameIndexというインデックスを作成しています。これにより、nameフィールドでの検索が高速に行えるようになります。インデックスは検索性能を劇的に向上させるため、大量のデータを扱う場合に特に有用です。


まとめ

IndexedDBは、Webブラウザに組み込まれたローカルデータベースで、以下の特徴があります:

  • 非同期操作:UIスレッドをブロックせず、バックグラウンドで動作。
  • キーと値のペア:データを一意のキーで管理し、複雑なデータ構造も扱える。
  • トランザクション制御とインデックス作成:データの整合性を保ちながら効率的にデータを管理。

このように、IndexedDBはWebアプリケーションにおけるデータ永続化に非常に役立つ技術であり、特にオフライン動作や大規模データを扱う場合に強力なツールとなります。次のセクションでは、IndexedDBを使用する際の基本的な操作方法や実装例を紹介します。

2. IndexedDBを使う前に知っておきたい基本概念

IndexedDBを効果的に利用するためには、いくつかの基本的なコンポーネントを理解しておく必要があります。これらの概念を押さえておくことで、データベースの構築や操作をスムーズに行えるようになります。以下では、IndexedDBを構成する主要なコンポーネントを説明します。


2.1 データベース(DB)

データベースは、IndexedDBの最上位の単位であり、データの保存場所となります。データベースを使って、アプリケーションのデータを永続的に保存することができます。

  • データベースの作成

    • データベースは、indexedDB.open()メソッドを使って開くことができます。このとき、バージョン番号を指定して新しいデータベースを作成したり、既存のデータベースをアップグレードしたりします。
    • データベースのバージョン管理を行い、必要に応じてデータ構造を変更できます。
  • データベースの管理

    • データベースには、一意な名前と、バージョン番号を付けることができます。バージョン番号を使ってデータベースの構造を管理し、バージョンが上がるたびにデータ構造を変更することができます。
  • const request = indexedDB.open("myDatabase", 1); // データベースを開く。バージョン1
    request.onupgradeneeded = function(event) {
      const db = event.target.result;
      console.log("Database created or upgraded!");
    };
    

このコードでは、"myDatabase"という名前のデータベースをバージョン1で開いています。onupgradeneededは、データベースが新規作成された場合やバージョンがアップグレードされた場合に呼ばれます。


2.2 オブジェクトストア(Object Store)

オブジェクトストアは、データベース内にあるデータの格納場所です。一般的に、オブジェクトストアは、テーブルに相当し、**レコード(データの単位)**を格納します。

  • データの格納

    • 各オブジェクトストアには、格納するデータのキー(例えばID)が必要です。これを使って、データを一意に識別します。
    • データは、オブジェクトストアにオブジェクト形式で保存されます。これにより、複雑なデータ構造も簡単に保存できます。
  • オブジェクトストアの作成

    • createObjectStore()メソッドを使用して、データベースに新しいオブジェクトストアを作成します。オブジェクトストアには、**keyPath(キーの属性)autoIncrement(自動的にインクリメントする)**の設定を指定できます。
  • const request = indexedDB.open("myDatabase", 1);
    request.onupgradeneeded = function(event) {
      const db = event.target.result;
      // オブジェクトストアを作成
      const store = db.createObjectStore("myStore", { keyPath: "id", autoIncrement: true });
      console.log("Object Store created!");
    };
    

このコードでは、"myStore"という名前のオブジェクトストアを作成しています。keyPath: "id"を指定することで、id属性をキーとしてデータを保存します。また、autoIncrement: trueを指定することで、idが自動的にインクリメントされます。


2.3 トランザクション(Transaction)

トランザクションは、複数のデータベース操作を一貫性を持って実行するための仕組みです。トランザクションを使用することで、データの整合性を確保できます。例えば、データの追加・更新・削除を一度に行う際に、途中でエラーが発生した場合でも全ての操作をロールバックし、データの矛盾を防ぐことができます。

  • トランザクションの特徴

    • readwrite:データの読み書きができるトランザクション。
    • readonly:データの読み取り専用のトランザクション。書き込み操作はできません。
  • トランザクションの使用方法

    • トランザクションは、データ操作を行いたいオブジェクトストアを指定して開始します。
  • const transaction = db.transaction(["myStore"], "readwrite");
    const store = transaction.objectStore("myStore");
    
    const record = { id: 1, name: "Alice", age: 30 };
    store.put(record); // データを追加・更新
    
    transaction.oncomplete = function() {
      console.log("Transaction completed!");
    };
    
    transaction.onerror = function() {
      console.log("Transaction failed!");
    };
    

このコードでは、"myStore"オブジェクトストアを対象にしたトランザクションを開始しています。トランザクションが完了したらoncompleteイベントが呼ばれ、失敗した場合はonerrorが呼ばれます。


2.4 インデックス(Index)

インデックスは、データベース内のデータを高速に検索するための仕組みです。インデックスを作成すると、特定のフィールドに対して素早く検索を行うことができ、パフォーマンスの向上につながります。

  • インデックスの作成

    • createIndex()メソッドを使用して、オブジェクトストアにインデックスを追加できます。インデックスは、検索対象となる**属性(フィールド)**を指定します。
  • インデックスのメリット

    • 大量のデータを取り扱う際、インデックスを利用することで、特定の条件でデータを検索する際のパフォーマンスが向上します。
  • const store = db.createObjectStore("myStore", { keyPath: "id" });
    store.createIndex("nameIndex", "name", { unique: false });
    

このコードでは、nameフィールドに対してインデックスを作成しています。これにより、nameでの検索が高速になります。また、unique: falseとすることで、nameが重複することを許可しています。


まとめ

IndexedDBを使用するためには、以下の基本的な概念を理解しておくことが重要です:

  • データベース(DB):データの保存場所として、データベースを作成します。
  • オブジェクトストア(Object Store):データの格納場所で、実際にデータが保存されるテーブルに相当します。
  • トランザクション(Transaction):複数のデータ操作を一貫して実行するための仕組みで、データの整合性を保ちます。
  • インデックス(Index):データ検索を高速化するための仕組みで、特定のフィールドに対してインデックスを作成します。

3. ネイティブでIndexedDBを使う基本的なコード

IndexedDBの操作には、いくつかの基本的な流れがあります。ここでは、データベースのオープンからデータの読み書きまでを順を追って紹介します。実際の実装に役立つコードを示しながら、どのように操作するのかを理解しましょう。


3.1 データベースのオープン

データベースを利用するために、まずはデータベースを開く必要があります。indexedDB.open()を使ってデータベースを開きます。

  • onupgradeneeded イベントは、データベースの作成やバージョンアップ時に呼ばれます。このタイミングでオブジェクトストア(テーブル)の作成を行います。
  • onsuccess イベントは、データベースが正常にオープンした後に呼ばれます。
  • onerror イベントは、データベースのオープンが失敗した場合に呼ばれます。

コード例:データベースを開く

let db;
const request = indexedDB.open("myDatabase", 1);

request.onupgradeneeded = function(event) {
  db = event.target.result;
  if (!db.objectStoreNames.contains("myStore")) {
    db.createObjectStore("myStore", { keyPath: "id" });
  }
};

request.onsuccess = function(event) {
  db = event.target.result;
  console.log("Database opened successfully");
};

request.onerror = function(event) {
  console.error("Database failed to open", event);
};
  • indexedDB.open("myDatabase", 1)で、myDatabaseという名前のデータベースをバージョン1で開いています。
  • onupgradeneededで、myStoreというオブジェクトストアが存在しない場合に新たに作成します。keyPath: "id"は、idを主キーとして使用する設定です。
  • データベースが正常に開かれると、onsuccessが呼ばれ、dbオブジェクトに対して操作を行う準備が整います。

3.2 データの書き込み

IndexedDBにデータを保存するには、トランザクションを使って操作を行います。トランザクションは、データ操作の一貫性を保つために利用されます。

  • putメソッドを使ってデータを追加または更新します。指定したidが既に存在する場合は更新され、存在しない場合は新しく追加されます。

コード例:データの書き込み

function saveData(data) {
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");
  const request = store.put(data); // put: データがなければ追加、あれば更新

  request.onsuccess = function(event) {
    console.log("Data saved successfully");
  };

  request.onerror = function(event) {
    console.error("Error saving data", event);
  };
}

const data = { id: 1, name: "John", age: 30 };
saveData(data);
  • db.transaction(["myStore"], "readwrite")で、myStoreオブジェクトストアを書き込み可能なトランザクションで取得します。
  • store.put(data)で、dataをオブジェクトストアに保存します。idフィールドが自動的にキーとして使われ、データが追加または更新されます。
  • 成功した場合はonsuccessが呼ばれ、失敗した場合はonerrorが呼ばれます。

3.3 データの読み込み

IndexedDBに保存されたデータを取得するには、getメソッドを使用します。これにより、指定したキー(id)に関連するデータを取得することができます。

  • getメソッドは、指定したIDのデータを非同期的に取得します。
  • 成功すると、データがrequest.resultに格納され、失敗するとonerrorが呼ばれます。

コード例:データの読み込み

function getData(id) {
  const transaction = db.transaction(["myStore"], "readonly");
  const store = transaction.objectStore("myStore");
  const request = store.get(id); // get: IDで指定されたデータを取得

  request.onsuccess = function(event) {
    if (request.result) {
      console.log("Data retrieved:", request.result);
    } else {
      console.log("Data not found");
    }
  };

  request.onerror = function(event) {
    console.error("Error retrieving data", event);
  };
}

getData(1); // IDが1のデータを取得
  • db.transaction(["myStore"], "readonly")で、myStoreオブジェクトストアを読み取り専用のトランザクションで取得します。
  • store.get(id)で、指定したIDのデータを取得します。データが存在すればrequest.resultにデータが格納されます。
  • データが見つからない場合は「Data not found」が表示され、エラーが発生した場合はonerrorが呼ばれます。

3.4 まとめ

  • データベースのオープンindexedDB.open()でデータベースを開く。
  • データの書き込みtransactionを使ってデータを追加または更新。putメソッドを利用。
  • データの読み込みtransactionでデータを読み取り、getメソッドで指定したIDのデータを取得。

4. 非同期処理を意識したエラーハンドリング

IndexedDBの操作はすべて非同期で行われるため、エラーハンドリングを適切に行わないと、意図しない挙動を引き起こす可能性があります。非同期処理を扱う際には、成功時と失敗時の処理を分けて記述することが非常に重要です。ここでは、onsuccessonerror イベントを使用して、エラー処理を効果的に行う方法を解説します。


4.1 非同期処理の流れとエラーハンドリング

IndexedDBでは、すべてのデータ操作(データベースのオープン、データの追加、取得、削除など)が非同期で実行されます。そのため、非同期操作が完了する前に次の処理が進んでしまうことを防ぐため、onsuccessonerror を使って、各処理が完了した後に適切な動作をするように記述します。

これらのイベントを使うことで、データ操作の成功や失敗に応じた処理の分岐が可能になります。

  • onsuccess:操作が正常に終了した場合に呼び出されます。
  • onerror:操作が失敗した場合に呼び出されます。

4.2 データベースのオープン時のエラーハンドリング

データベースのオープン時には、onsuccessonerrorの両方を使ってエラーハンドリングを行うことが重要です。例えば、データベースがすでに存在しているか、アクセスに問題がある場合に備えて、エラー処理を加えることができます。

コード例:データベースのオープン時のエラーハンドリング

let db;
const request = indexedDB.open("myDatabase", 1);

request.onupgradeneeded = function(event) {
  db = event.target.result;
  // オブジェクトストアの作成
  if (!db.objectStoreNames.contains("myStore")) {
    db.createObjectStore("myStore", { keyPath: "id" });
  }
};

request.onsuccess = function(event) {
  db = event.target.result;
  console.log("Database opened successfully");
};

request.onerror = function(event) {
  console.error("Error opening database:", event.target.error);
  alert("Failed to open database. Please try again.");
};
  • onsuccess:データベースが正常に開かれると呼ばれ、dbに対して操作を行う準備が整います。
  • onerror:データベースのオープンに失敗した場合、エラーメッセージを表示し、必要に応じてリカバリ処理を行います。エラー内容はevent.target.errorから取得できます。

4.3 データの書き込み時のエラーハンドリング

データをIndexedDBに保存する際、putメソッドを使用しますが、非同期で実行されるため、成功時と失敗時の処理を明確に分けることが重要です。onsuccessで成功時の処理を行い、onerrorで失敗時の処理を行います。

コード例:データの書き込み時のエラーハンドリング

function saveData(data) {
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");
  const request = store.put(data); // put: データがなければ追加、あれば更新

  request.onsuccess = function(event) {
    console.log("Data saved successfully");
  };

  request.onerror = function(event) {
    console.error("Error saving data:", event.target.error);
    alert("Failed to save data. Please try again.");
  };
}

const data = { id: 1, name: "John", age: 30 };
saveData(data);
  • onsuccess:データが正常に保存されると呼ばれ、保存が完了したことをログとして出力します。
  • onerror:データの保存に失敗した場合、エラーメッセージを表示し、リカバリ処理を行います。

4.4 データの読み込み時のエラーハンドリング

データを取得する際にも、非同期でデータが返されるため、エラーハンドリングをきちんと行うことが大切です。getメソッドを使用してID指定でデータを取得する際、成功時と失敗時の処理を分けて記述します。

コード例:データの読み込み時のエラーハンドリング

function getData(id) {
  const transaction = db.transaction(["myStore"], "readonly");
  const store = transaction.objectStore("myStore");
  const request = store.get(id); // get: IDで指定されたデータを取得

  request.onsuccess = function(event) {
    if (request.result) {
      console.log("Data retrieved:", request.result);
    } else {
      console.log("Data not found for ID:", id);
      alert("Data not found. Please check the ID.");
    }
  };

  request.onerror = function(event) {
    console.error("Error retrieving data:", event.target.error);
    alert("Failed to retrieve data. Please try again.");
  };
}

getData(1); // IDが1のデータを取得
  • onsuccess:データが正常に取得されると呼ばれ、request.resultに格納されたデータが利用可能になります。

    • データが存在しない場合は、ユーザーに対して「データが見つからない」旨を通知します。
  • onerror:データ取得に失敗した場合、エラーメッセージを表示し、リカバリ処理を行います。


4.5 トランザクションのエラーハンドリング

トランザクションが関与する操作(複数のデータベース操作を一度に実行する場合)では、トランザクション全体が成功した場合のみ変更が反映されますが、失敗するとすべての操作が取り消されます。

  • **トランザクションのoncompleteonerror**イベントを利用して、成功と失敗の処理を分けます。

コード例:トランザクションのエラーハンドリング

const transaction = db.transaction(["myStore"], "readwrite");
const store = transaction.objectStore("myStore");

transaction.oncomplete = function() {
  console.log("Transaction completed successfully!");
};

transaction.onerror = function(event) {
  console.error("Transaction failed:", event.target.error);
  alert("Transaction failed. Please try again.");
};

// データ操作例
const record = { id: 2, name: "Bob", age: 25 };
store.put(record);
  • oncomplete:トランザクションが正常に完了した場合に呼ばれます。すべての操作が成功したことを確認できます。
  • onerror:トランザクションの中で一部でも失敗した場合に呼ばれ、全体がロールバックされます。

4.6 まとめ

非同期処理を伴うIndexedDBの操作では、**onsuccessonerror**を活用することで、操作の結果を正しく処理できます。適切なエラーハンドリングを行うことで、アプリケーションの安定性が向上し、ユーザーに対しても適切なフィードバックを提供できます。これにより、データベース操作が失敗した場合でも、アプリケーションの動作がスムーズに保たれます。

エラーハンドリングを意識することで、トランザクション管理やデータ操作の信頼性を向上させ、ユーザーにより良い体験を提供できるようになります。

5. IndexedDBのパフォーマンス最適化

IndexedDBは、Webブラウザに組み込まれたローカルストレージ機能の一部ですが、容量やパフォーマンスに限界があります。データが多くなると、検索や読み込みの速度が遅くなることがあるため、適切なパフォーマンス最適化が非常に重要です。

このセクションでは、IndexedDBを使用する際にパフォーマンスを向上させるための方法について解説します。


5.1 インデックスの活用

インデックスは、データベース内で特定のフィールドを高速に検索するための仕組みです。インデックスを作成することで、検索が線形検索ではなく、効率的な探索で行われるため、大量のデータを扱う際に非常に有用です。

インデックスを活用することで、特定のフィールドに基づいた検索を高速化し、パフォーマンスを向上させることができます。

インデックスの作成

createIndex()メソッドを使用して、オブジェクトストアにインデックスを作成できます。インデックスは、検索対象となるフィールドを指定し、データの検索を効率化します。

  • uniqueオプション:インデックス内で一意の値を持たせるかどうかを設定します。unique: trueと指定すれば、インデックス内の値が重複しないことが保証されます。

コード例:インデックスの作成

const store = db.createObjectStore("myStore", { keyPath: "id" });
store.createIndex("nameIndex", "name", { unique: false });

このコードでは、"name"フィールドに基づいたインデックス"nameIndex"を作成しています。インデックスを作成することで、nameフィールドでの検索が高速化されます。ここでは、unique: falseを指定して、nameフィールドの値が重複しても問題ないようにしています。

インデックスの活用方法

インデックスを作成したら、検索時にインデックスを使ってデータを取得することができます。インデックスを使用することで、より効率的に検索を行えるようになります。

const store = db.transaction("myStore", "readonly").objectStore("myStore");
const index = store.index("nameIndex"); // インデックスを使用
const request = index.get("John"); // "John"という名前のデータを検索

request.onsuccess = function(event) {
  console.log("Found:", request.result);
};

この例では、"nameIndex"というインデックスを使って、name"John"のデータを検索しています。インデックスを使用することで、データの検索が効率的に行えます。


5.2 データ圧縮

IndexedDBに格納するデータが非常に大きい場合、データ圧縮を行ってストレージの使用量を抑え、読み書き速度を向上させることができます。

  • 圧縮のメリット

    • ストレージ容量の節約:大きなデータを圧縮することで、ストレージの使用量を削減できます。
    • 読み込み速度の向上:圧縮されたデータは、メモリ上で扱うサイズが小さくなるため、データの読み込みが高速化することがあります。
  • 圧縮方法: 圧縮方法には、JSON.stringifyでデータを文字列化し、その後圧縮ライブラリ(例えばpakoなど)を使用して圧縮する方法があります。圧縮後、データをIndexedDBに保存し、必要に応じて解凍して利用します。

コード例:データの圧縮と解凍

// データを圧縮して保存
function compressData(data) {
  const jsonData = JSON.stringify(data); // オブジェクトを文字列に変換
  const compressedData = pako.deflate(jsonData, { level: 9 }); // pakoを使用して圧縮
  return compressedData;
}

// 保存したデータを解凍
function decompressData(compressedData) {
  const decompressedData = pako.inflate(compressedData, { to: 'string' }); // 解凍
  const data = JSON.parse(decompressedData); // 文字列からオブジェクトに変換
  return data;
}

// データを保存する例
function saveCompressedData(data) {
  const compressedData = compressData(data);
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");
  const request = store.put({ id: 1, data: compressedData });

  request.onsuccess = function(event) {
    console.log("Compressed data saved successfully");
  };

  request.onerror = function(event) {
    console.error("Error saving compressed data", event);
  };
}

// データを取得する例
function getCompressedData(id) {
  const transaction = db.transaction(["myStore"], "readonly");
  const store = transaction.objectStore("myStore");
  const request = store.get(id);

  request.onsuccess = function(event) {
    if (request.result) {
      const decompressedData = decompressData(request.result.data);
      console.log("Decompressed data:", decompressedData);
    }
  };

  request.onerror = function(event) {
    console.error("Error retrieving data", event);
  };
}

このコードでは、pakoライブラリを使ってデータを圧縮してIndexedDBに保存し、後で解凍して取り出しています。これにより、大きなデータを効率的に管理できるようになります。

  • pako.deflate:データを圧縮します。
  • pako.inflate:圧縮されたデータを解凍します。

圧縮されたデータは、ディスクの容量を節約し、必要に応じて解凍して利用することができます。


5.3 データのバッチ操作

大量のデータを一度に操作する場合、バッチ操作を活用するとパフォーマンスが向上します。IndexedDBでは、複数のデータ操作をトランザクション内でまとめて実行できるため、バッチ処理を利用することで一度に多くのデータを効率的に扱うことができます。

コード例:バッチ処理

function batchInsertData(dataArray) {
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");

  dataArray.forEach(data => {
    store.put(data); // データをバッチで追加
  });

  transaction.oncomplete = function() {
    console.log("Batch insert completed successfully");
  };

  transaction.onerror = function(event) {
    console.error("Batch insert failed", event);
  };
}

const dataArray = [
  { id: 1, name: "Alice", age: 30 },
  { id: 2, name: "Bob", age: 25 },
  { id: 3, name: "Charlie", age: 35 }
];

batchInsertData(dataArray);

このコードでは、複数のデータを一度のトランザクションでまとめて追加しています。これにより、データ操作の回数が減少し、パフォーマンスが向上します。


5.4 まとめ

IndexedDBのパフォーマンスを最適化するための主な方法は以下の通りです:

  • インデックスの活用:検索対象となるフィールドにインデックスを作成し、データの検索を高速化。
  • データ圧縮:大きなデータを圧縮して保存し、ストレージ容量を節約。必要に応じて解凍して使用。
  • バッチ処理:複数のデータ操作をまとめて実行することで、効率的なデータ操作を実現。

これらの方法を活用することで、IndexedDBをより効率的に利用できるようになります。データの量が増える前にこれらの最適化を施しておけば、後々パフォーマンス問題に悩まされることなく、スムーズにアプリケーションを動作させることができます。

6. IndexedDBのデータ削除

IndexedDBに格納したデータは、一定期間経過したり、もはや必要なくなったりすることがあります。そのようなデータを削除することは、ストレージの無駄を避け、アプリケーションのパフォーマンスを維持するために重要です。

データを削除する際には、削除対象のIDを指定して、deleteメソッドを使用します。データを削除する処理は非同期で行われるため、エラーハンドリングも適切に行う必要があります。


6.1 データ削除の基本操作

データの削除は、transactionを使ってオブジェクトストア(テーブル)を操作し、deleteメソッドで指定したIDのデータを削除します。この際、onsuccessonerrorイベントを使って処理の結果を受け取ります。

コード例:データの削除

function deleteData(id) {
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");
  const request = store.delete(id); // delete: IDで指定されたデータを削除

  request.onsuccess = function(event) {
    console.log("Data deleted successfully");
  };

  request.onerror = function(event) {
    console.error("Error deleting data", event);
  };
}

deleteData(1); // IDが1のデータを削除
  • db.transaction(["myStore"], "readwrite")

    • myStoreというオブジェクトストアを読み書きモードで開きます。削除操作は書き込み操作にあたるため、"readwrite"モードを指定します。
  • store.delete(id)

    • 指定したIDを持つデータを削除します。idは削除対象のデータのキーです。
  • onsuccess

    • データが正常に削除された場合に呼ばれます。成功時には削除完了メッセージを表示します。
  • onerror

    • 削除操作が失敗した場合に呼ばれます。失敗した場合のエラーメッセージを表示します。

6.2 複数データの削除(バッチ削除)

特定の条件に一致するデータを一括で削除することもできます。例えば、ある期間内に保存されたデータや、特定のフラグが立っているデータを一括で削除するようなシナリオです。

コード例:複数データの削除(バッチ削除)

function deleteExpiredData() {
  const transaction = db.transaction(["myStore"], "readwrite");
  const store = transaction.objectStore("myStore");

  const request = store.openCursor(); // カーソルを使って全データを走査

  request.onsuccess = function(event) {
    const cursor = event.target.result;
    if (cursor) {
      const data = cursor.value;
      const currentTime = new Date().getTime();

      // 例えば、データが1ヶ月以上古い場合に削除
      if (data.timestamp && (currentTime - data.timestamp) > (30 * 24 * 60 * 60 * 1000)) {
        store.delete(cursor.key); // 古いデータを削除
        console.log(`Data with ID ${cursor.key} deleted`);
      }
      cursor.continue(); // 次のデータに進む
    }
  };

  request.onerror = function(event) {
    console.error("Error scanning data for deletion", event);
  };
}
  • openCursor()

    • カーソルを使ってオブジェクトストア内のデータを順番に処理できます。カーソルはデータベース内を前後に移動し、各データを取得します。
  • cursor.continue()

    • カーソルを使って次のデータに進みます。これにより、全てのデータを一つずつ確認できます。
  • store.delete(cursor.key)

    • もし削除条件(この例ではデータのタイムスタンプが1ヶ月以上前である場合)を満たす場合、deleteを使ってデータを削除します。

6.3 データの削除とパフォーマンス

大量のデータを削除する際には、データベースのパフォーマンスにも影響が出る可能性があるため、効率的な削除方法を考慮することが重要です。

  • バッチ処理を活用:複数のデータを一度に削除する場合、トランザクションを使ってまとめて操作を行うことで、処理時間を短縮できます。
  • カーソルを使った削除:カーソルを使って一つずつデータを確認し、条件に合致するデータを削除する方法は、非常に有効です。ただし、データ量が多すぎると、パフォーマンスが低下する可能性があるため、データ量に応じて適切に最適化しましょう。

6.4 まとめ

データ削除の基本操作は非常に簡単ですが、削除対象の条件や削除処理の方法を工夫することで、より効率的にストレージを管理することができます。

  • 単一データの削除store.delete(id)を使って指定したIDのデータを削除。
  • 複数データの削除(バッチ削除):カーソルを使って一度に多くのデータを削除。
  • パフォーマンスに配慮:削除対象のデータ量に応じて、効率的に削除操作を行う。

適切なデータ削除の実装は、アプリケーションのパフォーマンスやストレージ管理をより良くし、ユーザーの体験を向上させるために非常に重要です。

7. まとめ

IndexedDBをネイティブで操作することは、Web開発における非同期処理データベース管理の理解を深めるために非常に有益な学習機会です。しかし、実際の開発においては、開発効率メンテナンス性を重視する場合、ライブラリを活用する方が圧倒的に効率的です。

ネイティブでの実装は柔軟性があり、より詳細な制御が可能ですが、同時にエラーハンドリングパフォーマンス管理を意識してコードを書く必要があります。これらを適切に行うことで、IndexedDBを扱う際の信頼性安定性を高めることができます。

ライブラリ活用の提案

ライブラリを使用することで、IndexedDBの非同期操作やデータベース管理をさらに簡単に行うことができ、コードの可読性保守性が向上します。特に、以下のライブラリを使うことで、IndexedDBの操作が格段にシンプルになります。

  • Dexie.js

    • Dexie.jsは、IndexedDBの操作を簡略化するラッパーライブラリで、Promiseベースで非同期処理ができ、複雑な操作も直感的に扱えるようになります。これにより、非同期のエラーハンドリングやトランザクションの管理も非常に簡単になります。
  • localForage

    • localForageは、IndexedDB、WebSQL、LocalStorageなどのWebストレージ技術を統一的に扱えるライブラリで、データベース操作を簡素化します。複数のストレージ技術を抽象化しているため、バックエンドのストレージ技術が変更されてもアプリケーション側のコードに影響を与えることなく運用できます。
  • IDB

    • IDBは、IndexedDBのAPIを簡単に使えるようにしたラッパーライブラリで、エラーハンドリングや非同期操作の管理が非常に容易です。特に、非同期操作をPromiseでラップしているので、同期的なコードと同じように書けるため、非常に便利です。

これらのライブラリを活用することで、データベース操作が簡単になりコード量が削減され、バグのリスクも減少します。ライブラリを使うことで、開発者はビジネスロジックやユーザー体験の向上に集中できるようになります。


最終的な結論

  • ネイティブでのIndexedDB操作は学習には有益ですが、実際の開発においてはライブラリの活用を強くお勧めします。ライブラリを使うことで、開発がスピードアップし、コードの可読性と保守性が向上します。
  • エラーハンドリングパフォーマンス管理は、ネイティブ実装においては特に重要です。ライブラリを使うと、これらの問題を簡単に解決できます。
  • Dexie.jslocalForageなどのライブラリを使うことで、データベース操作がシンプルかつ効率的になり、開発の生産性が向上します。

これらのアプローチをうまく組み合わせることで、IndexedDBを使用したアプリケーション開発がよりスムーズに進み、より信頼性の高いシステムを構築できるようになります。