IndexedDBを試してみる

IndexedDBを試してみる

ブラウザ上のデータを保存したい場合FileSystemAPIがありますが、これは非推奨のようなので代わりに使えそうなのを探していたところIndexedDBが良さそうなので試して見たいと思います。

IndexedDBの使い方はmozillaのサイトを見たらわかると思います。

公式サイトを参考に以下のように実装して見ました。

function DbService() {
  var dbConnect;
  const dbName = "my_db";
  const storeName = "my_store";
  const dbVersion = 1;

  function setDbCon(connect) {
    dbConnect = connect;
  }
  function initFunc() {
    const request = indexedDB.open(dbName, dbVersion);
    request.onerror = (event) => {
      console.log("Error init db");
    };
    request.onupgradeneeded = createDb;
    request.onsuccess = (event) => {
      const db_con = event.target.result;
      setDbCon(db_con);
    };
  }
  function createDb(event) {
    const db_con = event.target.result;
    var objectStore = db_con.createObjectStore(storeName, { keyPath: "id" });
    setDbCon(db_con);
  }
  function command(datas, action, errorAction) {
    const transaction = dbConnect.transaction([storeName], "readwrite");
    transaction.onerror = errorAction;
    var store = transaction.objectStore(storeName);
    datas.forEach(d => action(store, d));
  }
  function query(keys, action, errorAction) {
    const transaction = dbConnect.transaction([storeName], "readonly");
    transaction.onerror = errorAction;
    var store = transaction.objectStore(storeName);
    keys.forEach(k => {
      var req = store.get(k);
      req.onsuccess = action;
    });
  }
  function cursor(action) {
    var transaction = dbConnect.transaction([storeName], "readonly");
    var objectStore = transaction.objectStore(storeName);
    objectStore.openCursor().onsuccess = function (event) {
      var cursor = event.target.result;
      if (cursor) {
        action(cursor);
        cursor.continue();
      }
    };
  }
  function clear() {
    dbConnect.close();
    const req = indexedDB.deleteDatabase(dbName);
    req.onerror = function (event) {
      console.log("Error deleting database.");
      initFunc();
    };
    req.onsuccess = function (event) {
      console.log("Database deleted successfully");
      initFunc();
    };
  }
  return {
    init: () => {
      initFunc();
    },
    insert: (datas) => {
      command(datas,
        (store, d) => {
          store.put(d);
        }, (event) => {
          console.log("insert error");
          console.info(event);
        });
    },
    keysAction: (keys, action, errorAction) => {
      query(keys, action, errorAction);
    },
    cursorAction: (action) => {
      cursor(action);
    },
    dataClear: () => {
      clear();
    },
    getConnection: () => {
      return dbConnect;
    }
  };
}

const dbService = DbService();
dbService.init();

ここではdbServiceでdbの作成、管理、それからデータの登録、削除、選択を行なっています。

dbServiceの初期化処理として以下は以下になります。indexedDB.open(dbName, dbVersion)でデータベースを開いています。新規でデータベースが作成される場合はonupgradeneededが実行されます、onupgradeneededは第二引数で渡すバージョン番号を変えることでも呼び出されます。onsuccessはopenの操作に成功した時点で呼び出されます。

init: () => {
  initFunc();
}
function initFunc() {
  const request = indexedDB.open(dbName, dbVersion);
  request.onerror = (event) => {
    console.log("Error init db");
  };
  request.onupgradeneeded = createDb;
  request.onsuccess = (event) => {
    const db_con = event.target.result;
    setDbCon(db_con);
  };
}

onupgradeneededのタイミングで呼び出されるobjectstoreの作成処理として以下の関数を実行しています。objectStoreにはindexを登録していてjson形式のデータを保存することができます。ここでkeyPathはRDBMSでの主キーに該当しデータの取得や削除の時に指定するキーになります。

function createDb(event) {
  const db_con = event.target.result;
  var objectStore = db_con.createObjectStore(storeName, { keyPath: "id" });
  setDbCon(db_con);
}

ここではkeyPathのみ指定していますが、検索条件に使うためのインデックスを追加する場合は以下のように指定ができます。インデックスはuniqueかどうかも選択できます。当たり前かもしれませんが検索に使わない不要なインデックスは張らないようにするべきだと思われます。

function createDb(event) {
  const db_con = event.target.result;
  var objectStore = db_con.createObjectStore(storeName, { keyPath: "id" });
  objectStore.createIndex("info1", "info1", { unique: false });
  objectStore.createIndex("info2", "info2", { unique: false });
  setDbCon(db_con);
}

それから、indexedDB.openのリクエストの成功時とonupgradeneededのタイミングでdbの接続情報をクロージャ内の変数にセットしています。

全データの削除処理について、以下のようにobjectStoreのclearメソッドを呼び出せばデータはindexedDBのAPIでデータの参照はできなくなるのですが開発者ツールChromeであればApplicationタブのClear Storageの項目を見るとストレージ領域自体は解放されていないことが確認できます。

db_con.transaction([storeName], "readwrite").objectStore(storeName).clear();

ストレージ領域を解放する場合はおそらく以下のようにdbを削除すれば大丈夫だと思います。自分が確認した限りですと無茶なインデックスを張っていなければdbの削除でストレージ領域は確保されるようでした。

dataClear: () => {
  clear();
},
function clear() {
  dbConnect.close();
  const req = indexedDB.deleteDatabase(dbName);
  req.onerror = function (event) {
    console.log("Error deleting database.");
    initFunc();
  };
  req.onsuccess = function (event) {
    console.log("Database deleted successfully");
    initFunc();
  };
}

ちなみにChromeでのindexedDBの保存先は以下になっているようです。データの登録削除が実際に反映されることが確認できると思います。

データの登録として以下の関数を作成しています。

insert: (datas) => {
  command(datas,
    (store, d) => {
      store.put(d);
    }, (event) => {
      console.log("insert error");
      console.info(event);
    });
},
function command(datas, action, errorAction) {
  const transaction = dbConnect.transaction([storeName], "readwrite");
  transaction.onerror = errorAction;
  var store = transaction.objectStore(storeName);
  datas.forEach(d => action(store, d));
}

ここではcommand関数でreadWriteのトランザクションを作成しておきリストの各データにinsert関数が渡した関数を実行することでデータの登録を行なっています。ここでstore.putを実行していますがstore.put自体はすでにキーが登録済みであれば更新し、なければ登録する動きをします。

全データに対して処理は以下のように行なっています。

cursorAction: (action) => {
  cursor(action);
},
function cursor(action) {
  var transaction = dbConnect.transaction([storeName], "readonly");
  var objectStore = transaction.objectStore(storeName);
  objectStore.openCursor().onsuccess = function (event) {
    var cursor = event.target.result;
    if (cursor) {
      action(cursor);
      cursor.continue();
    }
  };
}

cursorActionにはデータを取得後に実行する関数を渡すのですが、コンソール出力するのであれば以下のように実行できます。

dbService.cursorAction((result) => console.info(result.value));

動作検証

動作検証としてまずdbを作成して接続情報を取得しておきます。

const dbService = DbService();
dbService.init();

それからdbの作成が非同期で行われるので完了してから以下のようにデータの登録と内容確認が行えます。

const chars = [];
for(let code = "a".charCodeAt(0); code <= "z".charCodeAt(0); code++){
  chars.push(String.fromCharCode(code));
}
for(let code = "A".charCodeAt(0); code <= "Z".charCodeAt(0); code++){
  chars.push(String.fromCharCode(code));
}
for(let code = "0".charCodeAt(0); code <= "9".charCodeAt(0); code++){
  chars.push(String.fromCharCode(code));
}
var length = chars.length;

function randomStr(n) {
  var ret = "";
  for(let i = 0; i < n; i++) {
    ret += chars[Math.floor(Math.random()*length)];
  }
  return ret;
}


for(let i = 0; i < 100; i++) {
  dbService.insert([{
    "id": i,
    "info1": randomStr(80000),
    "info2": randomStr(32),
    "info3": randomStr(32),
    "info4": randomStr(32),
    "info5": randomStr(32),
  }]);
}
dbService.cursorAction((result) => console.info(result.value));
dbService.dataClear();
dbService.cursorAction((result) => console.info(result.value));

不要なインデックスは張らない方が良いとしましたが、例えばdbの作成時にinfo1、info2、info3、info4、info5に対してインデックスを張らない場合ではストレージ領域が7.7Mなのに対し張った場合は24Mと3倍以上に増えていてデータが増えるとさらに処理が重くなっていくことが予測できるので不要なdbは張らないほうが良さそうです。この辺りはMongoDBなどのNoSQLに慣れていれば自然に使えそうな気がしました。

後、indexedDBを使う注意点として確認はできていないのですがindexedDBで大量に保存するとlocalStorageなど他のstorageで上限が来るようになって使えなくなるかもしれないので注意した方が良さそうです。