見出し画像

IndexedDBで遊んでみた

こんにちは、MODEでソフトウェアエンジニアをしている木村です。

MODEではフルスタックエンジニアとして働いてます。ここ最近はReactを中心にやっていますが、最近プロジェクトでブラウザ内蔵型データベースのIndexedDBを知る機会があったので、学んだことのアウトプットも兼ねて記事を書いてみました!

IndexedDBを知ったきっかけ

将来的なマーケティングに活用するために、とある屋外イベントの特定層の入場データを取得したい課題を抱えていたユーザーに、MVP(Minimum-Viable-Product)を提供するプロジェクトにアサインされたのが最初のきっかけです。

今回の運用環境は、屋外イベントで沢山の人が1箇所に集まり通信が不安定になるリスクがあったのと、ユーザー側としては(当たり前ですが)データの欠損を出したくないニーズがありました。つまり通信が不安定な時にクライアント側にデータを一時保存して、安定したらデータをサーバー側に送るような処理が必要になる可能性が出てきました。

解決策のリサーチをしていて、ブラウザ上にデータベースを作るなんて面白い技術だなと興味を持って、とにかく触ってアウトプットをしてみるか!と思いこの記事を書きました。

IndexedDBの概要

ざっくりIndexedDBとは、ブラウザ内にデータを永続的にデータを保存するトランザクショナルデータベースです。大量の構造化されたデータ、オブジェクト等をクライアント側に保存する、あるいはオフラインでも動くアプリケーションを作るケースに最適です。localStorageよりも大量のデータを格納することができます。

詳しくはこちらを読んでください。

IndexedDBの使い方

通信が不安定な場合を想定して簡単なアプリをReactで作りました。入場データを格納するリクエストが、もしHTTPエラーを返却した場合にデータをIndexedDBに格納して保存します。

function App() {  

 // 処理#1
  const INDEXED_DB_NAME = "TEST_DATABASE";
  const STORE_NAME = "TEST_STORE"
  const request = indexedDB.open(INDEXED_DB_NAME, 1);
 
  // 処理#2 & #3
  request.onupgradeneeded = () => {
    console.log("Upgrading the database");
    const database = request.result;
    database.createObjectStore(STORE_NAME, {keyPath: "key"});
  }

  // 処理#4
  const insertData = (key: string, value: string) => {
    const request = indexedDB.open(INDEXED_DB_NAME);
    request.onsuccess = () => {
      const database = request.result;


      // 処理#5
      const trans = database.transaction(STORE_NAME, "readwrite");
      const store = trans.objectStore(STORE_NAME);
      store.add({key:key, value:value});
    }

    request.onerror = () => {
      console.log("Error data insert failed");
    }
  }

  // 処理#6
  const sendRequest = () => {
    const requestData = {timestamp: Date.now(), data: "This is test"}
    const method = "POST";
    const body = JSON.stringify(requestData);
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    };
    fetch("http://localhost:3000", {
      method, 
      headers, 
      body
    })
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch(() => insertData(`test-${requestData.timestamp}`, requestData.data))
  };

  return (
    <div className="App">
      <button onClick={sendRequest}>Send Request</button>
    </div>
  );
}

実行される処理の流れは以下のようになっています。

  1. ComponentがMountされるとINDEXED_DB_NAMEとSTORE_NAMEが初期化され、indexedDB.open関数がデータベースへのインターフェースを返却します。

  2. request.onupgradeneededは、データベースの更新(データ構造の変更等)を行う関数です。データベースが存在していなければ初期化(新規作成)します。onupgradeneededはバージョン更新時に実行される関数で、indexedDB.open({DB名}, {バージョン})の第2引数で指定できます。

  3. request.onupgradeneeded内のcreateObjectStore(STORE_NAME, {keyPath: "key"})が実行されデータを格納するobjectStoreが生成されます。第2引数の{keyPath: "key"}は、どの項目をキー(プライマリキーの様なもの)を設定するオブジェクトです。今回のケースではキーとして"key"を使用します。

  4. IndexedDBにinsertData関数を初期化します。まず関数内でデータベースへに接続します。request.onsuccessとrequest.onerrorの二つハンドラーがありますが、接続が成功した場合はrequest.onsuccessが実行され失敗した場合はrequest.onerrorが実行されます。

  5. 接続に成功したら、database.transaction(STORE_NAME, "readwrite")で"readwrite"(読み書きモード)で使用することを設定します。そして、trans.objectStore(STORE_NAME)でobjectStoreのインターフェースを取得し、add関数でデータをデータベースに挿入します。

    • "readwrite"以外に"readonly" (読み込み専用モード)もあります。

    • データを挿入するにはaddかputがあります。add関数は指定したキーが存在する場合はエラーを返し、put関数はキーがなければ挿入、あれば値を更新します。

  6. 最後にHTTPリクエストを送るsendRequestを初期化しますが、重要度は低いため説明は割愛します。リクエストに対してはレスポンスしてくれるサーバーがなければ、404が返ってきます。

Developer Console → Applications → Storage → TEST_DATABASE → TEST_STOREでデータが確認できます。

Developer Console上からデータを確認できる

疑問に思ったこと

別々のウィンドウで違うバージョンが走っていたらどうなる?

データベースの実際のバージョンより大きなバージョンを指定して open() を呼び出すときは、データベースに変更を施す前に、他にデータベースを開いているものが明示的に要求を認めなければなりません (それらを閉じるか再読み込みするまで、onblocked イベントが発生します)。

 出典:MDN Using IndexedDB ウェブアプリが別のタブで開かれているときにバージョンを変更する

容量はどれくらい?

基本的にはディスク空き容量によって変わるようで、ディスク空き容量の50%までのようです。

ブラウザーのストレージの最大容量は動的であり、ハードドライブのサイズに応じて変わります。グローバルリミットはディスクの空き量量の 50% に決められます。Firefox では、クォータマネージャーと飛ばれる内部のブラウザーツールがオリジンごとにどれだけディスク容量を使用しているかを絶えず注視しており、必要に応じてデータを削除します。

出典: MDN ブラウザーのステーレジ制限と削除基準

ライブラリは?

localForage: クライアント側のデータストレージ向けに、シンプルな name:value 形式の構文を提供するポリフィルです。バックグラウンドで IndexedDB を使用しますが、IndexedDB をに対応していないブラウザーでは Web SQL (非推奨)や localStorage にフォールバックします。
Dexie.js: 優良でシンプルな構文により高速なコード開発を可能にする、IndexedDB のラッパーです。
JsStore: SQL 風の構文による IndexedDB のラッパーです。
MiniMongo: クライアント側のインメモリーの mongodb で localstorage と server sync over http を元にしたもの。MiniMongo は MeteorJS で使われています。
PouchDB: クライアント側のブラウザー内の CouchDB 実装で IndexedDB を使っています。
IDB: IndexedDB API をほぼ反映した小さなライブラリーですが、使いやすさを大きく変える小さな改良が加えられています。
idb-keyval: IndexedDB で実装された超シンプルで小さな (~600B) プロミスベースのキーバリューストア
$mol_db: 小さな (~1.3kB) TypeScript のファサードで、プロミスベースの API と自動マイグレーションを備えています。
RxDB IndexedDB の上に使用することができる NoSQL クライアントサイドデータベースです。インデックス、圧縮、レプリケーションに対応して います。また、 IndexedDB にクロスタブ機能やオブザーバー機能を追加しています。

出典: MDN Using IndexedDB ライブラリー

まとめ・感想

通信が不安定な環境でも確実にデータを取得するために、MODEのゲートウェイ(エッジ側にデバイス)には一時的にデータを保存し解消された後に送信する機能が実装されています。同様の機能をブラウザに実装してみるのは個人的に面白かったですし、IndexedDBはどのような使い方をされているのかが非常に気になり始めています。一番わかりやすい例は、動画や画像データをIndexedDBに格納して表示を早くするケースでしょうか。

余談ですが、ネットが無い時どうするか?の逆でどんな環境でもネット繋げるぜ!アプローチの検証を、カスタマー・プロジェクトマネージャーの佐藤さんがStarlinkを検証した記事があるので読んでみてください。

MODEでは、業務拡大に伴い積極採用中しています!
我こそは!という方がいれば採用ページをご覧になってください。

ありがとうございました!