IndexedDB のバージョンアップと複数タブ問題

IndexedDB には、標準でバージョン管理機能が組み込まれています。

そのため、データベースの構造に変更を加える際には、「バージョンアップ」という特別な処理が発生します。

この記事では、標準 IndexedDB API を使ううえで必ず理解しておきたい

  • バージョンアップとは何か
  • バージョンアップで発生する問題
  • onblocked イベント
  • onversionchange イベント

の 4 点を中心に整理します。


バージョンアップとは何か

IndexedDB におけるバージョンアップとは、データベースの構造(スキーマ)を変更することを意味します。

ここでいうデータベースの構造を変更するとは、次のようなものを指します。

  • Object Store(テーブル)の追加・削除
  • Index(インデックス)の追加・削除
  • keyPath の変更
  • autoIncrement の設定変更

これらはすべて indexedDB.open() のパラメータのバージョン番号を上げないと実行できません。

具体例:従来の v1 のデータベースに、新たに Object Store を追加する。

const request = indexedDB.open("mydb", 2);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  db.createObjectStore("users", { keyPath: "id" });
};

具体例:従来の v1 のデータベースに、新たに Index を追加する。

const request = indexedDB.open("mydb", 2);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = event.target.transaction.objectStore("users");
  store.createIndex("by_name", "name");
};

👉 indexedDB.open() のバージョンを v1 → v2 に変更してオープンした後、onupgradeneeded イベント内で追加する必要があります。

尚、保存するオブジェクトのプロパティが増えるだけなら、構造は変わらないためバージョンアップは不要です。


バージョンアップで発生する問題

大前提として、IndexedDB は、複数のタブからのアクセスが可能です。その為、同一ドメインである場合、同一のデータベースに同時にアクセスすることになります。

そこで、既にデータベースをオープンしているタグとは異なるタブで、該当データベースの構造変更を行おうとした場合、問題(コンフリクト)が発生します。

IndexedDB のデータベースのバージョンアップは IDBOpenDBRequestonupgradeneeded イベント内で行う必要があります。そして、この処理は排他的(exclusive) です。

つまり、複数のタブが同時に構造変更を行うことはできない。

さらに、古いバージョンのデータベースを開いているタブがあると、新しいバージョンへのアップグレードは進められないという制約があります。

注)「通常の読み書き」は、この排他的な影響を受けません。複数タブでの同時アクセスが可能です。

コンフリクトの典型例

  • タブ A:データベースを v1 を開いている
  • タブ B:データベースを v2 で open()(構造変更が必要)

👉 タブ A が古いデータベースを開いたままなので、タブ B のアップグレードが進まない。

このときに発生するのが IDBOpenDBRequestonblocked イベント と IDBDatabaseonversionchange イベント です。

IndexedDBには、コンフリクトに対応するバージョン管理機能が標準で用意されています。

これらに適切な処理を記述することで、データベースの構成変更時に柔軟な対応を行うことが可能です。


onblocked イベント

データベースの構造変更を行おうとする側が、バージョンを変更して、indexedDB.open() を行った際に、既に古いバージョンでのオープンが別のところで行われていた場合、発生します。

const request = indexedDB.open("mydb", 2);

request.onblocked = () => {
  console.log("他のタブが古いDBを開いているため、アップグレードできません");
};

👉 メッセージ表示後、他のタブで開かれているデータベースが、close() されるまで、処理が中断し、待たされます。

onblocked イベントは、「エラー」ではありません。

onblocked イベントが発生した場合、open() の処理は、onsuccess にも onerror にもならず、待ち状態のまま停止します。

他のタブがデータベースを閉じれば、open() はそのまま続行されるため、“待ち状態の通知” にすぎないのです。


onversionchange イベント

データベースをオープンしている状態で、別のところからデータベースの構造変更を行おうとした際に、IDBDatabase の通知として発生します。

これは、IndexedDB が標準で持っているタブ間通知の仕組みです。

db.onversionchange = () => {
  console.log("他のタブが新しいバージョンでDBを開こうとしています");
  db.close(); // 道を開ける
};

古いバージョンのデータベースを開いたままの為、他タブでの構造変更を妨害しています。この場合、データベースを閉じれば、アップグレードが進みます。

データベースの構造変更を行いたい側からは、「閉じてください」と onversionchange イベントにて通知を行いますが、ブラウザが強制的にデータベースを閉じることはありません。db.close() を呼ばない限り、他タブのアップグレードは進みません。

その為、データベースの構造変更を円滑に進める為には、onversionchange イベント内で、db.close() を呼ぶ処理が必要になります。


まとめ

IndexedDB バージョンアップの流れ

例:タブBでデータベースの構造変更を実施

┌───────────────────────────────┐
│          タブA               │          タブB               │
└───────────────────────────────┘

  ① open(v1)

  ② onversionchange  ←───── ① open(v2)
    (他タブを妨害している)
          │
          └─→db.close()         ② onblocked
                  │                  (自分が待たされる)
                  │
                  └──────→ ③ onupgradeneeded
                                      (アップグレードが続行される)

IndexedDB のバージョンアップと複数タブ問題は、標準 API を使ううえで避けて通れない重要な概念です。

概念 意味 誰が待たされている?
onblocked 自分の構造変更が進められない 自分
onversionchange 他タブの構造変更を妨害している 他タブ

この 2 つを理解すると、 IndexedDB のバージョン管理と複数タブ間の問題の挙動が自然に理解でき、より安全で堅牢なアプリ設計ができるようになります。