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>
);
}
実行される処理の流れは以下のようになっています。
ComponentがMountされるとINDEXED_DB_NAMEとSTORE_NAMEが初期化され、indexedDB.open関数がデータベースへのインターフェースを返却します。
request.onupgradeneededは、データベースの更新(データ構造の変更等)を行う関数です。データベースが存在していなければ初期化(新規作成)します。onupgradeneededはバージョン更新時に実行される関数で、indexedDB.open({DB名}, {バージョン})の第2引数で指定できます。
request.onupgradeneeded内のcreateObjectStore(STORE_NAME, {keyPath: "key"})が実行されデータを格納するobjectStoreが生成されます。第2引数の{keyPath: "key"}は、どの項目をキー(プライマリキーの様なもの)を設定するオブジェクトです。今回のケースではキーとして"key"を使用します。
IndexedDBにinsertData関数を初期化します。まず関数内でデータベースへに接続します。request.onsuccessとrequest.onerrorの二つハンドラーがありますが、接続が成功した場合はrequest.onsuccessが実行され失敗した場合はrequest.onerrorが実行されます。
接続に成功したら、database.transaction(STORE_NAME, "readwrite")で"readwrite"(読み書きモード)で使用することを設定します。そして、trans.objectStore(STORE_NAME)でobjectStoreのインターフェースを取得し、add関数でデータをデータベースに挿入します。
"readwrite"以外に"readonly" (読み込み専用モード)もあります。
データを挿入するにはaddかputがあります。add関数は指定したキーが存在する場合はエラーを返し、put関数はキーがなければ挿入、あれば値を更新します。
詳しく知りたい場合はObjectStoreの仕様書から確認してください。
最後にHTTPリクエストを送るsendRequestを初期化しますが、重要度は低いため説明は割愛します。リクエストに対してはレスポンスしてくれるサーバーがなければ、404が返ってきます。
Developer Console → Applications → Storage → TEST_DATABASE → TEST_STOREでデータが確認できます。
疑問に思ったこと
別々のウィンドウで違うバージョンが走っていたらどうなる?
容量はどれくらい?
基本的にはディスク空き容量によって変わるようで、ディスク空き容量の50%までのようです。
ライブラリは?
まとめ・感想
通信が不安定な環境でも確実にデータを取得するために、MODEのゲートウェイ(エッジ側にデバイス)には一時的にデータを保存し解消された後に送信する機能が実装されています。同様の機能をブラウザに実装してみるのは個人的に面白かったですし、IndexedDBはどのような使い方をされているのかが非常に気になり始めています。一番わかりやすい例は、動画や画像データをIndexedDBに格納して表示を早くするケースでしょうか。
余談ですが、ネットが無い時どうするか?の逆でどんな環境でもネット繋げるぜ!アプローチの検証を、カスタマー・プロジェクトマネージャーの佐藤さんがStarlinkを検証した記事があるので読んでみてください。
MODEでは、業務拡大に伴い積極採用中しています!
我こそは!という方がいれば採用ページをご覧になってください。
ありがとうございました!