html5

IndexedDBにストアしたオブジェクトのキー値を部分的に更新する

投稿日:2019年5月28日 更新日:

はじめに

IndexedDBは「key : value」でレコードを保存するキーバリューストアです。

バリューには「単値」または「Javascriptオブジェクト」をストア出来ます。RDBのようなレコード構造を持たせたいならJavascriptオブジェクトを持たせます。恐らくこちらの方が利用シーンが多いのではないでしょうか。

valueが単値の場合

mail(key)name(value)
hoge@xyz.co.jp“hogeさんの名前”

valueがオブジェクトの場合

mail(key:mail)Object(value)
hoge@xyz.co.jp{
 mail: “hoge@xyz.co.jp”,
 name: “hogeさんの名前”,
 address: {
  prefecture: “東京”,
  distriction: “港区”
 },
 memo: “備考事項”,
 regist: Dateオブジェクト
}

値がオブジェクトの方が1レコードでより多くの情報を持てますし、オブジェクトの中にオブジェクトを作れば3次元的なレコードも表現保存出来ます。IndexedDBに対するCRUD操作はJavascriptオブジェクトそのままで行える為、LocalStrageのようにオブジェクトを一旦文字列に変換して保存したり、読み取ってからJSON.parse()してオブジェクトに戻す必要もありません。便利ですね。

ですが「ストアしたオブジェクトのこのキー値だけ更新したい」時は少し工夫した方がコードが分かり易い、という話です。

課題

valueをオブジェクトにした場合、登録、読込は簡単だけどRDBのUPDATE文のような特定カラムを更新するような処理はIndexedDBは苦手です。

IndexedDBはDBファイルへのwriteがvalue全体でアトミックに行われる為、valueオブジェクトの部分更新という概念がなく、value全体を再度writeする必要があります。更新の挙動を見てみます。更新はputメソッドで行います。

DB、ObjectStoreを作り、初期データを2件登録します。

        let dbVer = 1.0;
        let dbName = "testdb";
        let storeName = "members";

      (snip)

            let openRequest = indexedDB.open(dbName, dbVer);

            openRequest.onupgradeneeded = function (event) {
                db = event.target.result;

                // membersオブジェクトストア作成
                let store = db.createObjectStore(storeName, { keyPath: "mail" });

                // 初期データを2件投入
                store.put({
                    mail: "hoge@xyz.co.jp",
                    name: "hogeさんの名前",
                    address: {
                        prefecture: "東京",
                        distriction: "港区"
                    },
                    memo: "hogeさんの備考事項",
                    regist: new Date()
                });
                store.put({
                    mail: "foo@xyz.co.jp",
                    name: "fooさんの名前",
                    address: {
                        prefecture: "東京",
                        distriction: "江東区"
                    },
                    memo: "fooさんの備考事項",
                    regist: new Date()
                });
            }

Chromeのデベロッパーツール「Application」→「IndexedDB」でDB内容を確認すると2件登録されています。

hogeさんのmemoを部分更新する為、キーであるmailと、memoで構成されたオブジェクトをput()してみます。

        // IDBObjectStore.put()で更新
        function updateByPut(memo) {
            // testdbデータベースオープン
            let openRequest = indexedDB.open(dbName, dbVer);
            openRequest.onsuccess = (event) => {
                let db = event.target.result;
                let transaction = db.transaction([storeName], "readwrite");
                let store = transaction.objectStore(storeName);

                let request = store.put({
                    mail: "hoge@xyz.co.jp",
                    memo: memo
                });
            }
        }

結果、hogeさんのレコードはmailとmemoだけになり、他の情報が切り捨てられてしまいます。

与えたキー値だけ更新して欲しいのですが・・・バリュー全体がDBファイルにwriteされてしまう仕様なので致し方ない挙動ですね。

putを使った部分更新をする場合、オブジェクト全体をput()してやればよいことが分かりました。しかしアプリ側でオブジェクト全体を持っておかなければならなくなり、明らかに効率が悪いです。

より良い方法

Cursorを使って狙いのレコードをフェッチしてオブジェクト全体を取り、更新したいキー値を変更したのち、IDBCursor.update()をコールします。

今度はfooさんのmemoを更新してみます。

        // IDBCursor.update()で更新
        function updateByCursorUpdate(memo) {
            // testdbデータベースオープン
            let openRequest = indexedDB.open(dbName, dbVer);
            openRequest.onsuccess = (event) => {
                let db = event.target.result;

                // membersオブジェクトストア取得
                let transaction = db.transaction([storeName], "readwrite");
                let store = transaction.objectStore(storeName);

                // membersオブジェクトストアからfooさんのレコードをフェッチ
                let request = store.openCursor(IDBKeyRange.only("foo@xyz.co.jp"));
                request.onsuccess = (event) => {
                    let cursor = request.result;
                    if (cursor) {
                        // cursor.valueはfooさんのキー値Javascriptオブジェクト
                        // memoキー値を更新
                        cursor.value.memo = memo;
                        let updateRequest = cursor.update(cursor.value);
                        updateRequest.onsuccess = function () {
                            console.log("更新成功", updateRequest.result);
                        };
                        cursor.continue();
                    }
                }
            }
        }

    <input type="button" onclick="updateByCursorUpdate(this.value)"
        value="IDBCursor.update()で更新" />

今度は他のオブジェクトキー値は保たれたまま、memoだけ更新出来ました。

まとめ

MDNドキュメント内のput()メソッド解説にも、更新にはputではなくupdateの方が好ましいと記述があります。


Bear in mind that if you have a IDBCursor to the record you want to update, updating it with IDBCursor.update() is preferable to using IDBObjectStore.put(). Doing so makes it clear that an existing record will be updated, instead of a new record being inserted.

更新するレコードにIDBCursorがある場合は、IDBObjectStore.put()を使用するよりもIDBCursor.update()で更新するほうが望ましいことに注意してください。 そうすることで、新しいレコードが挿入されるのではなく、既存のレコードが更新されることが明らかになります。


https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put

結局update()も内部的には部分更新されたJavascriptオブジェクト全体をDBファイルにwriteしていると思われますが、確かに第三者がコードを見た時にputだと新規追加なのか更新なのか見分けがつかないのに対し、updateだと「あぁ、既存レコードの部分更新なんだな」が明確に分かるメリットが有りますね。

オブジェクトの一部を更新する為にオブジェクト全体をアプリ側で渡す効率の悪さも改善できます。

補足

動確用の全体コードを載せておきます。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>IndexedDB JSONキー値更新</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
        let dbVer = 1.0;
        let dbName = "testdb";
        let storeName = "members";

        // 初期化
        function initialize() {
            let openRequest = indexedDB.open(dbName, dbVer);

            openRequest.onupgradeneeded = function (event) {
                db = event.target.result;

                // membersオブジェクトストア作成
                let store = db.createObjectStore(storeName, { keyPath: "mail" });

                // 初期データを2件投入
                store.put({
                    mail: "hoge@xyz.co.jp",
                    name: "hogeさんの名前",
                    address: {
                        prefecture: "東京",
                        distriction: "港区"
                    },
                    memo: "備考事項",
                    regist: new Date()
                });
                store.put({
                    mail: "foo@xyz.co.jp",
                    name: "fooさんの名前",
                    address: {
                        prefecture: "東京",
                        distriction: "江東区"
                    },
                    memo: "備考事項",
                    regist: new Date()
                });
            }
        }

        // IDBObjectStore.put()で更新
        function updateByPut(memo) {
            // testdbデータベースオープン
            let openRequest = indexedDB.open(dbName, dbVer);
            openRequest.onsuccess = (event) => {
                let db = event.target.result;
                let transaction = db.transaction([storeName], "readwrite");
                let store = transaction.objectStore(storeName);

                let request = store.put({
                    mail: "hoge@xyz.co.jp",
                    memo: memo
                });
            }
        }

        // IDBCursor.update()で更新
        function updateByCursorUpdate(memo) {
            // testdbデータベースオープン
            let openRequest = indexedDB.open(dbName, dbVer);
            openRequest.onsuccess = (event) => {
                let db = event.target.result;

                // membersオブジェクトストア取得
                let transaction = db.transaction([storeName], "readwrite");
                let store = transaction.objectStore(storeName);

                // membersオブジェクトストアからfooさんのレコードをフェッチ
                let request = store.openCursor(IDBKeyRange.only("foo@xyz.co.jp"));
                request.onsuccess = (event) => {
                    let cursor = request.result;
                    if (cursor) {
                        // cursor.valueはfooさんのキー値Javascriptオブジェクト
                        // memoキー値を更新
                        cursor.value.memo = memo;
                        let updateRequest = cursor.update(cursor.value);
                        updateRequest.onsuccess = function () {
                            console.log("更新成功", updateRequest.result);
                        };
                        cursor.continue();
                    }
                }
            }
        }

    </script>
</head>

<body onload="initialize()">
    <input type="button" onclick="updateByPut(this.value)" 
        value="IDBObjectStore.put()で更新" />

    <input type="button" onclick="updateByCursorUpdate(this.value)" 
        value="IDBCursor.update()で更新" />
</body>

</html>

-html5

執筆者:

関連記事

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(前編)

目次1 はじめに2 検証環境3 Ionicプロジェクト作成4 PDFダウンロード、IndexedDB保存を実装5 実行してみる はじめに PDFをHTTPダウンロードするとHDDに保存されるか、外部ビ …

Android、iPhoneが向いている方向をWeb地図上で表現してみる(leaflet.js使用)

前回Javascriptでコンパス機能を実装して、Android、iPhoneの背面が向いている方角が数値(0~360:0が北)で分かるようになりました。 One IT Thing  8 P …

ブラウザから起動したカメラの撮影画像をjavascriptで圧縮【Compressor.js】

「モバイル用Webアプリで撮影したカメラ画像のファイルサイズが大きすぎる・・・」 そんな悩みは無いですか? 昨今カメラ会社の経営が傾くほどスマホのカメラ性能が向上、それに応じて年々ファイルサイズも増大 …

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(後編)

目次1 はじめに2 ビューワ機能を提供してくれるnpmモジュールを選別する3 実装開始3.1 ng2-pdfjs-viewerを使えるようにする3.2 PDFビューワコンポーネントを作る3.3 Hom …

hidden.inとSoftEtherで無料のビデオ会議環境を構築する

自粛が続いて会えない人も多くなり、人との交流が恋しくなりますね。 仕事ではミーティングをオンライン化するニーズが増え、以下メジャーなパブリックサービスを使っている人も増えていると思います。 Skype …

 

shingo.nakanishi
 

東京在勤、職歴2n年中年ITエンジニアです。まだ開発現場で頑張っています。

19歳(1996年)から書き始めたアウトプット用プライベートWeb日記数が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。

私と同じく、今後IT業界で生計を立てて行きたいと考えている方や、技術共有したいけど仲間が居なくて孤独、といった方と一緒に成長、知識共有して行けたら楽しいな、と思っています。