【Nuxt3】IndexedDBでの実装例と失敗例

運営者:そうすけ


愛媛出身の30代のブロガー兼ソフトウェアエンジニア
主にフロント・バックの開発を行っています。

趣味はガジェットと植物

IndexedDBでアプリを作った時のサンプル例と失敗事例をまとめました。

目次

IndexedDB サンプルアプリコード

書き込みページ

読み込みページ

読み取りレシピページのコード
<template>
    <TheContainer >
      <AppH1>レシピアプリ</AppH1>
      <AppH2>作成したレシピ</AppH2>
      <AppUI>
        <li v-for="link in links" :key="link.url">
          <AppLink :href="link.url">{{ link.text }}</AppLink>
        </li>
      </AppUI>
      <div>
        <ButtonPrimary :on-click="() => goWrite()">レシピを追加する</ButtonPrimary>
      </div>     
      
  
    </TheContainer>

</template>

<script setup lang="ts">

//レシピ一覧リンク。初期値は空の配列
//mountedでIndexedDBからすべてのレシピ名・IDを操作
//下記変数にpushする
const links = ref<{
  url:string;
  text:string;
}[]
>([]);

//テーブル名とレシピ名
const dbName ="recipe-memo"
const tableName = "recipe"

onMounted(()=>{
  const openRequest = indexedDB.open(dbName)
  alert('a'+openRequest.onsuccess);

  openRequest.onerror= (event) =>{
     alert('しっぱい');
  }

  openRequest.onsuccess= (event) =>{
    
    //コールバック関数からindexDBのインスタンスを作成
    const db = (event.target as IDBRequest).result;
    alert('せいこう');
    //閲覧のみなのでreadonlyトランザクションを開始する
     const transaction = db.transaction(tableName,"readonly");
    
     const table = transaction.objectStore(tableName) as IDBObjectStore;
   
    //DB内のレコードを一件ずつ巡回するためのカーソルをリクエスト
    const cursorRequest = table.openCursor();

    //カーソルの取得成功ごとに発火するイベント
    //カーソルをcontinue()で前進させた場合にも発火
    cursorRequest.onsuccess = (event) =>{
      const cursor = (event.target as IDBRequest).result;

      if(!cursor) return;

      //現在のカーソル位置のレコードを取得してレシピのリンク一覧に追加
      const record = cursor.value;
      links.value.push({
          //idからレシピ詳細ページのURLを作成
          //詳細ページはpages/products/recipe_memo/[id]/index.vueの動的ルート

          url:'/products/recipe_memo/${record.id}',
          //レシピ名
          text:record.name,
          
      })
      //次のカーソルに移動 (cursorRequest.onsuccessが呼ばれる)
      cursor.continue();

    }
  }
  //indecedDB初回
  openRequest.onupgradeneeded = (event) =>{
    const db =(event.target as IDBRequest).result;
    db.createObjectStore(tableName,{keyPath:"id"})
  }

})

const goWrite = () =>{
  navigateTo("/products/recipe_memo/write")
}

</script>
書き込みレシピのコード
<template>
    <TheContainer>
        <AppH1>レシピメモできるアプリ</AppH1>
        <div class="text-right"> 
            <ButtonPrimary :on-click="goBack">アプリTOPに戻る</ButtonPrimary>
        </div>       
        <AppH2>レシピを書く</AppH2>
        <RecipeMemoForm v-model="form" :id="v4()" redirect-on-success="/products/recipe_memo"></RecipeMemoForm>
    </TheContainer>
</template>

<script setup lang="ts">
import {v4} from "uuid";
import { type RecipeEntity } from "~~/types/entities";

//indexedDBのDB名とテーブル名
const dbName ="recipe-memo";
const tableName = "recipe";

//indexDBはブラウザ側なので、Mount時に初期化させる
onMounted(()=>{

    const openRequest = indexedDB.open(dbName)

    //indexxdDBの初回オープンでは、onupgradeneededイベントが発火する
    //このイベントの中なら、オブジェクトストアを作成したり、
    //DBの構造を変更することができる
    openRequest.onupgradeneeded = (event) =>{

    //コールバック関数からindexDBのインスタンスを作成
    const db = (event.target as IDBRequest).result
    

    //テーブル名とレコードの識別子となるキーの名前を指定して、
    //新たにテーブル(正確にはオブジェクトストア)を作成する
    db.createObjectStore(tableName,{keyPath:"id"});

    };
});

//レシピの実態であり、入力UIにv-modelで渡すフォームでもある。
//そのため新規登録用フォーム用の初期値を空にする

const form = reactive<RecipeEntity>({
    //レシピ名
    name:"",
    //材料グループ(初期状態は1つ。UI側から任意に増減可能)
    items:[
        {
        //材料名
        name:"",
        //量
        amount:"",
        //単位
        unit:""
        }
    ],
    howToCook:"",
});

//アプリのトップに戻る関数
const goBack = () => {
    navigateTo("/products/recipe_memo");
}

</script>
書き込みファイルの子コンポーネント
<template>
    <div class="grid gap-8">
        <label>
            レシピ名
            <InputText v-model="form.name" ></InputText>
        </label>
        <div v-for="(item,index) in form.items" class="bg-main-100 rounded-md p-4 shadow-md dark:bg-coffee dark:border-2 ">
            <div class="text-right">
                <ButtonSecondary :on-click="() => removeItem(index)">材料{{index+1}}を削除する</ButtonSecondary>
              
            </div>
            <div class="grid grid-cols-3 gap-2" >
                <label>材料{{ index +1 }}の名前
                <InputText v-model="item.name"></InputText>
                </label>
                <label>材料{{ index +1 }}の個数
                <InputNum v-model="item.amount"></InputNum>
                </label>
                <label>材料{{ index +1 }}の単位
                <InputText v-model="item.unit"></InputText>
                </label>
            </div>

            <div class="py-2">
                <ButtonSecondary :on-click="() => addItem()">材料を追加する</ButtonSecondary>
                
            </div>


        </div>
        <label>
            調理方法
            <InputTextarea v-model="form.howToCook" ></InputTextarea>
        </label>
        <div class="w-full text-center">
            <ButtonPrimary :on-click="() => submit()">レシピを保存する</ButtonPrimary>
        </div>
    </div>
  


</template>
<script setup lang="ts">

//入力フォームをまとめたコンポーネント
//親-自身-子で受け渡せるコンポーネントを作成

//レシピオブジェクト
interface RecipeModel {
    //料理名
    name:string;
    //材料名
    items:{
        name:string;
        amount:""|number;
        unit:string;        
    }[],
    //調理方法
    howToCook:string,
}
//propsの作成
const props = defineProps<{
    //v-modelで渡せるようmodelvalueを設定
    modelValue:RecipeModel;
    //レシピid
    id:string;
    //レシピの書き込み時にリダイレクトする関数
    redirectOnSuccess:string;
}>()

//Emitsの作成
const emits = defineEmits<{
    "update:modelValue":[value:RecipeModel]
}>()
//get,setで親とリアクティブにやりとり
const form = computed({
    get:()=>props.modelValue,
    set:(value)=>emits('update:modelValue',value)
})

//材料を削除する処理
const removeItem = (index:number) => {
 form.value.items.splice(index,1);
}
//追加処理
const addItem =() =>{
 form.value.items.push({
    name:"",
    amount:"",
    unit:"",
 })
}


//以下DB処理-----------
//INdexdDBのDB名とテーブル名
const dbName = "recipe-memo"
const tableName = "recipe";

//レシピ保存処理
const submit = () =>{
    console.log(props.modelValue)
    
    //from.valueの中味を分割代入でばらす
    const {name, items, howToCook} = form.value;

    //いずれかのフォームが空白ならエラー
    if(!name||
    !howToCook||
    items.some(
        (item) => !item.name || !Number.isFinite(item.amount)||!item.unit
    )    
    ){
        alert("いずれかのフォームが空白です");
        return;
    }

//IndexedDBを使うには、まずIndexedDBを開く必要がある
//なので最初にそのためのリクエストをopen関数で実行する
//その際、引数には使いたい任意のDB名を渡す。
const openRequest = indexedDB.open(dbName);
alert(openRequest.onsuccess );
//IndexedDBの軌道に成功したら、次のコールバック関数を実行
openRequest.onsuccess = (event) => {
    //起動しただけではレシピの保存をできない
    //まずコールバック関数の引数からIndexedDBのインスタンスを取得する
    //なお型推論が弱いので、より厳密な方を明示している
    const db =(event.target as IDBRequest).result;

    //トランザクションを開始
    const transaction =db.transaction(tableName,"readwrite");
  
    //テーブル名を指定して、IndexedDBからテーブルを取得する
    //(厳密にはテーブルでなくオブジェクトすとあ)
    //なおこのテーブルは親が事前に作成しておく前提
    //型推論が弱いので、厳密な方を明示
    const table =transaction.objectStore(tableName) as IDBObjectStore;
    
    //ここまででIndexedDBへの操作が可能になる
   
    //親から渡されたレシピID
    const id = props.id;

    //テーブルへのレシピ保存を試行する
    const putRequest = table.put({
        //親がidをindexedDBのキーとして使える用準備してある前提
        id,
        name,
        items: items.map((i) => ({
            //仕様上、シリアライズ可能値にすべきなのでmapする
            name: i.name,
            amount:i.amount,
            unit:i.unit,
        })),
        howToCook,
    });

    console.log(id);
    console.log(name);
    console.log(items);
    console.log(howToCook);
  
    //レシピ保存に成功したら、親から渡されたリダイレクト関数を実行
    putRequest.onsuccess = (event) => {
        const menu = (event.target as IDBRequest).result.value;
        console.log(menu);
        alert("保存に成功しました");
        navigateTo(props.redirectOnSuccess);
    };
    //レジピ保存に失敗したらアラートを出す
    putRequest.onerror = () =>{
        alert("保存に失敗しました");
    };
 };


};




</script>

エラー例

実装時、データの取得ができませんでした。

原因1:インサートができてない

onsuccessイベントは取得で来ていてonerrotは起こらないので、オブジェクトストア接続自体には成功していました。

put successも成功しており、書き込み処理自体も成功している可能性が高いと考えました。

原因2:データの取得ができない

初め型エラーで定義が間違っていたかと思いましたが、IndexedDBの使用上オブジェクトストア自体、NoSQLのため、厳密なテーブル定義や型定義はなさそうと考えました。

いろいろ調べた結果、cursor処理でカーソル変数がnullになっていました。

カーソルをやめてgetAllでで全件取得でデータを見ようとしました。

onsuccessは実行されるが、getAllでundefinedになり、データがはいっていないことを確認しました。

  openRequest.onsuccess= (event) =>{
    
    //コールバック関数からindexDBのインスタンスを作成
    const db = (event.target as IDBRequest).result;
    alert('せいこう');
    //閲覧のみなのでreadonlyトランザクションを開始する
    const transaction = db.transaction(tableName,"readonly");    
    const table = transaction.objectStore(tableName) as IDBObjectStore;

  //全件取得テスト
  table.getAll().onsuccess = (event) => {
  const alldate = (event.target as IDBRequest).result;
   console.log(alldate.key);
  };
  };

getコマンドでキーで取得してみたが、onsucusessが実行され、idでundefinedがでました。

//キーテスト
const transaction = db.transaction(tableName,"readonly");    
const table = transaction.objectStore(tableName) as IDBObjectStore;
const request = objectStore.get("1e02bcb8-f839-4477-9928-82c26cc774c9");
request.onerror = (event) => {
  // エラー処理!
};
request.onsuccess = (event) => {
  // request.result に対して行う処理!
  const a = (event.target as IDBRequest).result;
  alert(a.id);
};

menuはなぜかIDがでる

putRequest.onsuccess = (event) => {
        const menu = (event.target as IDBRequest).result;
        console.log(menu);
        alert("保存に成功しました");
        navigateTo(props.redirectOnSuccess);
    };
    //レジピ保存に失敗したらアラートを出す
    putRequest.onerror = () =>{
        alert("保存に失敗しました");
    };
 };

解決方法

ますIndexedDB自体を確認しましょう。
そのあと自分のコーディングを確認しましょう。

indexedDBの確認方法

ブラウザ上でCtr+Shift+Iを押し、開発者モードを開きます。タブの中に「Application」という項目があります。

その中の「Storage」にIndexedDBが存在します。

もしすでにオブジェクトストアを作成したりデータをいれていると、データを確認することができます。

私が起こしたエラーの原因

writeするオブジェクトストアとreadするオブジェクトストアをコードタイプミスしていました。

dbNameを”recipe-memo”にするところ、”recipe-name”にしておりました。
お恥ずかしい。。

//write画面のコンポーネントに記載していた変数
const dbName ="recipe-memo"
const tableName = "recipe"

//read画面のコンポーネントに記載していた変数
const dbName ="recipe-name"
const tableName = "recipe"

データが取得できないのにonerrorが起こらずonsuccessが実行されていたのは、readするオブジェクトストアが存在していたからです。

気を聞かせて、writeする画面でもreadする画面でもエラーにならないよう、初回はオブジェクトストアを作成するようにしていました。
初回時はonupgradeneededのイベントが実行されます。

 openRequest.onupgradeneeded = (event) =>{
    const db =(event.target as IDBRequest).result;
    db.createObjectStore(tableName,{keyPath:"id"})
  }

それが仇となって、オブジェクトストア名を間違えたことでwrite画面とread画面で2つオブジェクトストアを作ってしまっていました。

実際に”recipe-memo”と”recipe-name”というオブジェクトストアができていました。

getall関数やget関数でも、データはないがオブジェクトストア自体にはアクセスはできてしまうのでonsuccessコマンドが実行されていました。
もちろん戻り値はnull。

でもやgetでIDを指定してonsucceseが実行されるのに、データがないわけがないとループに陥っておりました。

まとめ

IndexedDBは複雑なコーディングではないので、エラーが起こったら以下のことを確認しましょう。

  • IndexedDBに使用ドメインのブラウザからアクセスしてデータを確認する。
  • コーディングを確認する

厳密なテーブル定義がないので、インサート時もDB側でインサートエラーが起きにくい仕組みです。

なのでアプリケーション側でチェックする設計が必要だと学びました。

参考書籍

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次