Vue.jsを学習し始めネット上に溢れる記事を見ながら自分でコードが書けるようになってきて次はデータベースへの読み書きだと思った時に記事にはデータベースへの読み書きの方法まで記述されていない。そんな経験ないですか?そのためVue.jsからデータベースに対して読み書きをするにはどうすればいいのだろうと悩んでいる人(または悩んでいた人)もいるかと思います。本文書ではそのような経験をしている人のためにVue.jsから下記の3つのデータベースへの読み書きを行う方法について説明を行なっています。

  • Supabase(PostgreSQL)
  • Firebase(Firestore)
  • Expressサーバ(Prisma + SQLite)

Supbase, Firebaseはクラウドサービスという共通的がありますがSupabaseのPostgreSQLはSQLデータベース、FirebaseのFirestoreはNO SQLデータベースという違いがあります。Expressサーバはクラウドサービスではなくバックエンドサーバを各自が用意するため一からセットアップと管理を行う必要があります。本文書を通して3つの異なったデータベースの理解も深めることができます。

基本的な手順については他のフロントエンドライブラリのReactやSvelteでも同じです。

プロジェクトの作成

npm init vueコマンド(npm create vue@latestでも同じ)を利用してVue.jsプロジェクトの作成を行います。コマンドを実行するとTypeScriptやVue Routerなどの機能の選択を行えますがここでは何も選択せずに進めます。


 % npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-js-database
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/mac/vue-js-database...

Done. Now run:

  cd vue-js-database
  npm install
  npm run dev

プロジェクト作成後は作成したディレクトリに移動してnpm installを実行し、ライブラリのインストールを行なってください。

デフォルトで設定されているスタイルを無効にするためにmain.jsファイルのmain.cssファイルのimport文を削除またはコメントとしておきます。


// import './assets/main.css'

import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');

タスク管理アプリケーションの作成

データベースの読み書きの方法を説明するためシンプルなタスク管理のアプリを作成します。最初にデータベースの読み書きがない場合のコードをApp.vueファイルに記述し、その後データベースに対して読み書きの行えるコードへと変更していきます。

コードはCompositions APIを利用して記述します。このコードを元にデータベースを利用した場合のコードに書き換えていきます。まずは下記のコードが正常に動作することを確認してから進めてください。


<script setup>
import { ref } from 'vue';
let id = 0;

const tasks = ref([]);
const task = ref('');

const addTask = () => {
  tasks.value.push({ id, task: task.value, complted: false });
  task.value = '';
  id++;
};

const deleteTask = (id) => {
  const index = tasks.value.findIndex((task) => task.id === id);
  tasks.value.splice(index, 1);
};
</script>

<template>
  <h1>Tasks</h1>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      :style="task.completed ? 'text-decoration:line-through' : ''"
    >
      <span><input type="checkbox" v-model="task.completed" /></span>
      <span>{{ task.task }}</span>
      <button @click="deleteTask(task.id)">削除</button>
    </li>
  </ul>
  <form @submit.prevent="addTask">
    <div>
      <input v-model="task" />
    </div>
    <div>
      <button type="submit">タスクを登録</button>
    </div>
  </form>
</template>

コードの内容を簡単に説明しておくとref関数でリアクティブな変数tasksとtaskを定義します。tasksには登録したタスク情報を配列で管理します。taskはinput要素からタスクを入力する際に利用します。idは重複しないようにタスクを追加する毎に+1しています。

adddTask関数ではinput要素に入力したタスクをtasksに追加しています。deleteTask関数ではidを利用して削除予定のタスクの配列内での要素番号を取得して要素番号を利用して配列から削除予定のタスクを含む要素を削除しています。

addTaskで登録したタスクはv-forディレクティブで展開してcheckboxを利用してcompletedの値をtrueからfalseまたはfalseからtrueに変更できるようにしています。completedがtrueの場合はタスクに横線を入れています。

タスク管理
タスク管理

タスク管理のアプリは作成できますがブラウザのリロードを行うと入力したデータは消えます。リロードでの値の保存だけであればローカルストレージを利用することができますが、登録したタスク情報を保存するためにデータベースを利用します。

ここまでが一般的なVue.jsでシンプルアプリを作成しようといった記事でよく公開されている内容です。本記事ではここからさらに進めてデータベースへの読み込みの機能を設定してより実践的なアプリケーションへと変更していきます。

Supabase編

クラウドベースのデータベースサービスSupabaseを利用することで自分でサーバを管理・構築することなくデータベースを利用することができます。データベースの操作にはSupabaseが提供するライブラリを利用することで直接Vue.jsからデータベースにアクセスすることができます。データベースにはリレーショナルデータベースのPostgresが使われています。

Supbaseを利用するためにSupbaseのサービスにサインアップを行うことが必須ですがフリープランが用意されているので安心して利用できます。

サインアップの方法について別記事で公開済みなので下記の記事を参考に行なってください。Githubのアカウントで簡単にサインアップを行うことができます。

プロジェクトの作成

サインアップが完了した状態から開始し、プロジェクトの作成から行っていきます。

プロジェクト作成画面が表示されるとNameにはプロジェクト名を設定します。任意の名前をつけることができるのでここではVuejsとしています。データベースのパスワードが必要となるのでパスワードを入力してRegionにはTokyoが存在するのでTokyoを選択します。プランはFreeのままで”Create new project”ボタンをクリックします。

プロジェクトの作成
プロジェクトの作成

プロジェクトの作成が完了するまでしばらく待ちます。作成中に画面にはプロジェクトで利用するURLとanonキーが表示されます。

APIキーとURLの表示
APIキーとURLの表示

プロジェクトの作成が完了するとAPIの情報は画面上から消えるので別の画面が表示されるのでその場合は左側にあるメニューの一番下の歯車のアイコンをクリックしてAPIの情報を確認してください。

APIキーの確認
APIキーの確認

環境変数の設定

Project URLとProjectのAPIのanonキーを環境変数として設定するのでVue.jsプロジェクトの直下に.envファイルを作成して以下の環境変数を設定してください。ブラウザ上からコピーを行い設定を行ってください。各自異なる値が設定されます。プロジェクトはViteを利用して構築されているので環境変数の先頭にはViteのルールに従ってVITE_がつきます。


VITE_SUPABASE_URL=YOUR_PROJECT_URL
VITE_SUPABASE_ANON_KEY=YOUR_PROJECT_ANON_KEY

VITEの環境変数についてはこちらから確認してください。

ライブラリのインストール

SupabaseをVue.jsからアクセスするためにはライブラリのインストールが必要になります。


% npm install @supabase/supabase-js

インストール後はSupabaseを利用するためのクライアントインスタンスの作成を行う設定ファイルを作成します。設定では環境変数で設定した値が必要になります。プロジェクトのsrcフォルダの下にsupabase.jsファイルを作成して以下を記述します。.envファイルの環境変数を読み込むためにはimport.meta.envオブジェクトを利用します。


import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

テーブルの作成

Supabaseの管理画面に戻りテーブルの作成を行います。テーブル名はtasksとして列はid, task, completed, created_atで構成します。

テーブルの作成は左側のメニューの上から2番目のTable editorで行います。画面中央に”Create a new table”ボタンが表示されているのでクリックします。

新規テーブルの作成
新規テーブルの作成

テーブルの名前をtasksとしてDescriptionにタスク用テーブルを入力します。Enable Row Level Securityがチェックされていますがテスト用に利用するだけなのでチェックを外しておきます。通常はチェックをしたままで進めますが追加の設定が必要となるためデータベースの読み書きを目的にしている本文書ではチェックを外しています。

テーブル名の設定
テーブル名の設定

さらにスクロールすると列名を入力する場所があるので下記のように設定します。設定ができたら”Save”ボタンをクリックします。

列の設定
列の設定

作成後テーブルの情報が表示されます。管理画面からもデータを挿入することができます。

テーブルの表示
テーブルの表示

“Insert Row”ボタンをクリックしてデータの挿入を行います。taskに適当なタスク名を設定して”Save”ボタンをクリックしてください。idとcreated_atは自動で設定され、completedはテーブル作成時のデフォルト値をfalseに設定したので自動でfalseが設定されます。

データの入力画面
データの入力画面

追加したデータが表示されます。

データ挿入後のテーブル
データ挿入後のテーブル

Supabaseの管理画面での設定は完了なのでここからVue.jsを利用してテーブルの管理を行っていきます。

データの読み込み

作成したsupabase.jsファイルをApp.vueファイルでimportします。

import後Supabaseで作成したtaskテーブルからデータを取得するためにgetTasks関数を実行します。fromメソッドでは取得したデータを持つテーブルを指定します。select(‘*’)を設定することですべての列情報を取得しています。select(‘id’)など*(アスタリスク)ではなく列名を指定することで個別の列情報を取得することもできます。


<script setup>
import { ref } from 'vue';
import { supabase } from './supabase';

const tasks = ref([]);
const task = ref('');

const getTasks = async () => {
  let { data, error, status } = await supabase.from('tasks').select('*');
  tasks.value = data;
};

getTasks();

const addTask = () => {
  tasks.value.push({ id, task: task.value, complted: false });
  task.value = '';
  id++;
};

const deleteTask = (id) => {
  const index = tasks.value.findIndex((task) => task.id === id);
  tasks.value.splice(index, 1);
};
</script>

<template>
  <h1>Tasks</h1>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      :style="task.completed ? 'text-decoration:line-through' : ''"
    >
      <span><input type="checkbox" v-model="task.completed" /></span>
      <span>{{ task.task }}</span>
      <button @click="deleteTask(task.id)">削除</button>
    </li>
  </ul>
  <form @submit.prevent="addTask">
    <div>
      <input v-model="task" />
    </div>
    <div>
      <button type="submit">タスクを登録</button>
    </div>
  </form>
</template>

ブラウザで確認するとSupabaseの管理画面で登録したタスクが画面上に表示されます。

Supabaseのテーブルから取得したデータの表示
Supabaseのテーブルから取得したデータの表示

もし表示されない場合はsupabase.fromメソッドから戻されるオブジェクトの中のerrorをコンソールに表示させてエラーの原因を確認してください。エラーがない場合はnullが表示されます。


const getTasks = async () => {
  let { data, error, status } = await supabase.from('tasks').select('*');
  console.log(error);
  tasks.value = data;
};

ここまでの設定でSupabaseのデータベースに保存したデータをVue.jsから読み込めるようになりました。

データの書き込み

データの書き込みを行うためaddTask関数を下記のように更新します。fromメソッドでデータを追加するテーブルを指定しinsertメソッドでinput要素に入力した値を指定します。select(‘*’)メソッドを追加することで追加したデータが配列で戻され、配列の1番目に追加したデータが入っています。


const addTask = async () => {
  const { data, error } = await supabase
    .from('tasks')
    .insert([{ task: task.value }])
    .select('*');
  tasks.value.push(data[0]);
  task.value = '';
};

addTask関数を更新後inputフィールドに文字列を入力します。

inputフィールドにタスクを入力
inputフィールドにタスクを入力

”タスク登録”ボタンをクリックして追加したタスクがブラウザ上に表示されることを確認してください。

タスクの追加
タスクの追加

タスク追加後にブラウザのリロードを行い追加したタスクが表示されることを確認してください。表示されればデータベースへの書き込みも問題なく行われています。

これでVue.jsからデータベースへの読み書きを行うことができるようになりました。

削除処理

データベースの削除処理も確認しておきます。deleteTask関数を下記のように更新します。supabaseのfromメソッドに削除を行うテーブルを指定しeqメソッドでidが一致するデータの削除を行なっています。


const deleteTask = async (id) => {
  const { data, error } = await supabase
    .from('tasks')
    .delete()
    .eq('id', id)
    .select('id');
  const index = tasks.value.findIndex((task) => task.id === data[0].id);
  tasks.value.splice(index, 1);
};

削除ボタンをクリックするとブラウザ上のタスクが削除されるだけでなくデータベース上のデータも削除されます。

削除ボタンをクリックした後にリロードを行い削除したデータがブラウザ上に表示されないか確認してください。

更新処理

データベースを利用していない場合はcheckboxのチェックをクリックするだけでv-modelで設定したcompletedの値がfalseからtrue, trueからfalseへの更新されました。データベースを利用している場合はデータベース上のデータを更新する必要があるため新たにchangeイベントとupdateTask関数を追加します。


<span
  ><input
    type="checkbox"
    v-model="task.completed"
    @change="updateTask(task)"
/></span>

supabaseのupdateメソッドでは更新が必要なcompletedの値のみ設定を行なっています。checkboxをクリックするとv-modelによりcompletedの値がfalseからtrueに更新されるので更新された値をupdateメソッドに指定しています。データはidの値を使って特定しています。selectメソッドにより更新したデータが配列で戻されるので最初の要素の値をメモリ上のtasksの更新に利用しています。


const updateTask = async (task) => {
  const { data, error } = await supabase
    .from('tasks')
    .update({ completed: task.completed })
    .eq('id', task.id)
    .select('*');
  const currentTask = tasks.value.find((task) => task.id === data[0].id);
  currentTask.completed = data[0].completed;
};

ここまでの動作確認でVue.jsとSupbaseを利用してデータベースのCRUD(Crete, Read, Update, Delete)を行うことができるようになりました。Supbaseを利用するとバックエンドサーバの構築・管理が必要ないので効率的にデータベースを操作するアプリケーションを作成することができます。

Firebase編

クラウドベースのFirebaseの中のFirestoreというサービスを利用することでVue.jsからデータベースを操作することができます。データベースの操作にはFirebaseが提供するライブラリを利用することで直接Vue.jsからデータベースにアクセスすることができます。FirestoreはNoSQL型のデータベースでキーと値のペアでデータを保存していきます。サーバの管理はFirebase側で行ってくれるのでサーバの設定/管理や必要ありません。

Firebaseを利用するためにGoogleアカウントが必要になりますがフリープランが用意されているので安心して利用できます。

Firebaseのプロジェクト作成

Googleアカウントにログインした状態でhttps://firebase.google.com/にアクセスします。トップ画面に”使ってみる”ボタンが表示されているのでクリックします。

Firebaseのトップ画面
Firebaseのトップ画面

Googleアカウントでログインしていない場合にはログインを行う必要があります。ログインが完了するとFirebaseへようこそ画面が表示されるので画面に表示されている”プロジェクトを作成”ボタンをクリックしてください。

プロジェクト名の設定画面が表示されるので任意の名前のプロジェクト名を入力して”続行ボタン”をクリックしてください。ここではvue.jsという名前を設定しています。

プロジェクト名の設定
プロジェクト名の設定

Googleアナリティクスの設定画面が表示されますが動作確認のため利用しないので無効にします。無効に設定後”プロジェクト作成”ボタンをクリックします。

Googleアナリティクスの設定画面
Googleアナリティクスの設定画面

プロジェクトの作成が完了するまでしばらく待ちます。プロジェクトの作成が完了したら画面中央に”続行”ボタンが表示されるので”続行 “ボタンをクリックしてください。プロジェクトの作成は完了です。

Firebaseを利用するためには接続するための情報とSDK(Software Development Kit)が必要となります。Firebase内のデータへのCRUD(Create, Read, Update, Delete)についてFirebaseが準備している SDKを使って操作を行います。利用するためにはアプリの開始が必要なのでプロジェクトの概要画面の中央にある”</>”ボタンをクリックしてください。

プロジェクト作成完了後の画面
プロジェクト作成完了後の画面

プロジェクトだけではなくアプリにも名前をつける必要があります。こちらも任意の名前をつけてください。ここではvuejsという名前をつけています。名前を入力したら”アプリを登録”ボタンをクリックしてください。

アプリの登録画面
アプリの登録画面

Firebase SDKの追加ということでfirebaseライブラリのインストール方法とFirebaseの設定方法が表示されます。

Firebaaseへの接続情報
Firebaaseへの接続情報

npmコマンドを利用してfirebaseをインストールしてください。


% npm install firebase

インストール後package.jsonファイルでインストールしたfirebaseのバージョンを確認しておきます。


  "dependencies": {
    "firebase": "^9.14.0",
    "vue": "^3.2.41"
  },

firebaseライブラリのインストール完了後にsrcディレクトリ直下にfirebase.jsファイルを作成して画面に表示されていた内容をコピー&ペーストします。下記の情報は利用できないのでコピー&ペーストを行わないでください。


// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyAgwo_kcpG2Izs6Tg2sIfje70pewRAOkUs",
  authDomain: "vuejs-ad0f2.firebaseapp.com",
  projectId: "vuejs-ad0f2",
  storageBucket: "vuejs-b0af.appspot.com",
  messagingSenderId: "1001390977387",
  appId: "1:1001190987387:web:53d3aAaf93C769f619f4de"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

設定が完了したら”コンソールに進む”ボタンをクリックしてください。次はCloud Firestoreのデータベースを作成するのでCloud Firestoreのボタンをクリックしてください。

Cloud Firestoreの選択
Cloud Firestoreの選択

Cloud Firestoreのページでは”データベースの作成”ボタンが表示されているのでクリックしてデータベースの作成を行います。

データベースの作成
データベースの作成

動作確認を行うのが目的なのでテストモードで開始するを選択して”次へ”ボタンをクリックしてください。

データベースの作成画面
データベースの作成画面

ロケーションはTokyoを選択することができるのでTokyoを選択します。選択後”有効にする”ボタンをクリックしてください。

ロケーションの選択
ロケーションの選択

データベースの作成が完了すると以下の画面が表示されます。

データベース作成後の画面
データベース作成後の画面

データベースへのデータの挿入

リレーショナルデータベースではデータベースを作成後、スキーマ(列情報の名前や型情報)を定義してテーブルの作成を行います。Firestoreではスキーマ情報を事前に設定することなくキーと値のペアであれば自由な形でデータを保存することができます。テーブルに対応するものとしてコレクションという入れ物がありその中のドキュメントにデータを保存していきます。

ドキュメントを保存するためにはコレクションが必須なので”コレクションを開始”をクリックしてコレクションの作成を開始します。コレクションの開始画面が表示されコレクションのIDを設定できます。ここではtasksを設定します。これから作成するドキュメントにアクセスする際にコレクションの名前を利用します。tasksを入力した後”次へ”ボタンをクリックします。

コレクションIDの設定
コレクションIDの設定

ドキュメントの追加画面が表示されるのでフィールドにtaskとcompletedを追加します。ドキュメントIDは自動IDをクリックすると文字列が表示され、それがIDとなります。

ドキュメントの追加
ドキュメントの追加

自動IDをクリックすると”保存”ボタンがクリックできるようになるので”保存”ボタンをクリックしてください。

tasksというコレクションにドキュメントを追加することができました。g4F….の文字列が自動で設定されたコレクションのIDです。

ドキュメントの保存
ドキュメントの保存

ドキュメント追加ボタンをクリックすることでさらにデータを追加することができますが先ほど説明した通りFirestoreではスキーマの設定を行わないのでドキュメント毎に自由にデータを入れることができます。

ドキュメントの追加画面
ドキュメントの追加画面

FirebaseのFirestoreにデータを保存することができたので保存したデータをVue.jsからアクセスしてブラウザ上に表示させます。

データの読み込み

firestoreを利用するためにfirebase.jsファイルにfirestoreを利用するための情報を追加します。定義したdbはコンポーネントから利用するためexportしておきます。


// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyAgwo_kcpG2Izs6Tg2sIfje70pewRAOkUs",
  authDomain: "vuejs-ad0f2.firebaseapp.com",
  projectId: "vuejs-ad0f2",
  storageBucket: "vuejs-b0af.appspot.com",
  messagingSenderId: "1001390977387",
  appId: "1:1001190987387:web:53d3aAaf93C769f619f4de"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

export const db = getFirestore();

作成したfirebase.jsファイルをApp.vueファイルでimportします。

import後firestoreで作成したtasksコレクションからドキュメントを取得するためにgetTasks関数を追加します。db以外にfirestoreのメソッドもimportを行なっています。collectionメソッドの第一引数にはdb, 第二引数にはコレクションの名前を設定します。getDocsでコレクションに保存させているすべてのドキュメントを取得しています。Promiseが戻されるので非同期関数のasync, awaitを利用しています。doc.data()でドキュメントに保存したキーと値の情報を取得できますがIDが含まれていないのでdoc.idでIDは別に取得しています。map関数で展開することで配列としてドキュメントの情報を取得しています。


<script setup>
import { ref } from 'vue';
import { db } from './firebase';
import { collection, getDocs } from 'firebase/firestore';

const tasks = ref([]);
const task = ref('');

const getTasks = async () => {
  const querySnapshot = await getDocs(collection(db, 'tasks'));
  tasks.value = querySnapshot.docs.map((doc) => ({
    ...doc.data(),
    id: doc.id,
  }));
};

getTasks();

const addTask = () => {
  tasks.value.push({ id, task: task.value, complted: false });
  task.value = '';
  id++;
};

const deleteTask = (id) => {
  const index = tasks.value.findIndex((task) => task.id === id);
  tasks.value.splice(index, 1);
};
</script>

<template>
  <h1>Tasks</h1>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      :style="task.completed ? 'text-decoration:line-through' : ''"
    >
      <span><input type="checkbox" v-model="task.completed" /></span>
      <span>{{ task.task }}</span>
      <button @click="deleteTask(task.id)">削除</button>
    </li>
  </ul>
  <form @submit.prevent="addTask">
    <div>
      <input v-model="task" />
    </div>
    <div>
      <button type="submit">タスクを登録</button>
    </div>
  </form>
</template>

実行するとデベロッパーツールのコンソールに”Uncaught (in promise) FirebaseError: Missing or insufficient permissions.”が表示された場合にはCloud Firestoreのルールを下記の画像の設定に変更してください。writeの横に設定されているif falseを削除するかif trueに変更してください。これは今回のようにVue.jsからデータベースを読み書きことを目的にすべての読み書きを許可しているので通常はルールを設定する必要があります。

Firestoreのセキュリティルールの変更
Firestoreのセキュリティルールの変更

アクセスを許可した後、ブラウザ上にはFirestoreのドキュメントに追加したタスクが表示されます。

Firestoreデータベースからの読み込み
Firestoreデータベースからの読み込み

Vue.jsからFirestoreデータベースにアクセスしてデータベースに保存されたデータを読み込みことができるようになりました。

リアルタイムでの読み込み

コレクションに新たなドキュメントが追加されたり、更新が行われた際に追加更新後のデータをリアルタイムで受け取ることができます。リアムタイムで読み込むためにはgetDocsメソッドではなくonSnapshotメソッドを利用します。


const getTasks = async () => {
  const unsubscribe = onSnapshot(collection(db, 'tasks'), (querySnapshot) => {
    tasks.value = querySnapshot.docs.map((doc) => ({
      ...doc.data(),
      id: doc.id,
    }));
  });
};

onSnapshotメソッドでは、JavaScriptのaddEventListenerがイベントをリッスンするようにデータの更新をリスナーがリッスンするためコンポーネントをアンマウントする際にリスナーを解除する必要があります。解除にはonSnapshotの戻り値を利用します。

リスナーの解除はライフサイクルフックのonUnmountedで行います。


<script setup>
import { ref, onUnmounted } from 'vue';
import { db } from './firebase';
import { collection, onSnapshot } from 'firebase/firestore';

const tasks = ref([]);
const task = ref('');

const getTasks = async () => {
  const unsubscribe = onSnapshot(collection(db, 'tasks'), (querySnapshot) => {
    tasks.value = querySnapshot.docs.map((doc) => ({
      ...doc.data(),
      id: doc.id,
    }));
  });
};

getTasks();

//略

onUnmounted(() => {
  unsubscribe();
});
</script>

ブラウザ上にgetDocsメソッドを利用した場合と同様にドキュメンに保存されていたタスクが表示されます。

データの書き込み

データの書き込みを行うためaddTask関数をaddDocメソッドを利用して下記のように更新します。新規でタスクを追加した場合にはcompletedの値はfalseになるように設定しています。


import { collection, onSnapshot, addDoc } from 'firebase/firestore';
//略
const addTask = async () => {
  await addDoc(collection(db, 'tasks'), {
    task: task.value,
    completed: false,
  });
  task.value = '';
};

入力フォームにタスクを入力後、”タスクを登録”ボタンをクリックすると登録したデータがブラウザ上に表示されます。理由はonSnapshotメソッドを利用してコレクションの更新をリッスンしているためです。getDocsメソッドを利用している場合には登録したデータが即座に表示されることはありません。

削除処理

データベースの削除処理も確認しておきます。タスクが持つidを利用して削除するドキュメントを指定しています。


import {
  collection,
  doc,
  addDoc,
  onSnapshot,
  deleteDoc,
} from 'firebase/firestore';
//略
const deleteTask = async (id) => {
  await deleteDoc(doc(db, 'tasks', id));
};

削除ボタンをクリックするとonSnapshotメソッドを利用してコレクションの更新をリッスンしているため削除ボタンをクリックしたタスクが即座にブラウザ上から削除されます。

更新処理

データベースを利用していない場合はcheckboxのチェックをクリックするだけでv-modelで設定したcompletedの値がfalseからtrue, trueからfalseへ更新されました。データベースを利用している場合はデータベース上のデータを更新する必要があるため新たにchangeイベントとupdateTask関数を追加します。


<span
  ><input
    type="checkbox"
    v-model="task.completed"
    @change="updateTask(task)"
/></span>

データの更新にはsetDocメソッドを利用して行います。


import {
  collection,
  doc,
  setDoc,
  addDoc,
  onSnapshot,
  deleteDoc,
} from 'firebase/firestore';
//略
const updateTask = async (task) => {
  await setDoc(doc(db, 'tasks', task.id), {
    task: task.task,
    completed: task.completed,
  });
};

ここまでの動作確認でVue.jsとFirebaseのFirestoreを利用してデータベースのCRUD(Crete, Read, Update, Delete)を行うことができるようになりました。

Expressサーバ編(Prisma + SQLite)

Supbase, Firebaseはどちらもクラウドベースのサービスでサーバの構築を行う必要がありませんでした。ここではNode.jsのExpresサーバ用を準備してデータベースへの接続を行います。Vue.jsからアクセスは一度Expressサーバを経由してデータベースへアクセスすることになります。

データベースにはSQLiteを利用しますがExpressサーバとSQLiteデータベースの間にORMであるPrismaを利用します。Prismaを利用することでSQLではなくPrisma上で定義するモデルオブジェクトのメソッドを通してデータベースの操作を行います。Prismaを利用した場合はSQLiteだけではなくMySQLやPostgres, mongoDBデータベースの接続にも利用することができます。またSupabaseではsupabase.jsライブラリを利用しましたがPrismaを経由してアクセスすることもできます。

Prismaのインストール

任意の名前のExpressサーバ用のフォルダを作成します。


 % mkdir backend-express

backend-expressフォルダに移動後、npmコマンドを利用してprismaのライブラリをインストールします。


 % npm install prisma 

Prisma用の設定ファイルを作成するためにinitコマンドを実行します。


% npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

コマンドを実行するとprismaフォルダが作成されその中にPrismの設定ファイルであるschema.prismaファイルが作成されます。

schema.prismaファイルではデフォルトでPostgreSQLの設定が行われているのでSQLに変更を行います。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite" //デフォルトではpostgresql
  url      = env("DATABASE_URL")
}

環境変数DATABASE_URLも変更が必要なので.envファイルを開いてSQLiteデータベースファイルを指定します。dev.dbファイルを作成する必要はありません。この後のPrismaコマンドを実行すると自動で作成されます。


DATABASE_URL="file:./dev.db"

SQLiteはリレーショナルデータベースでschema.prismaファイルにデータモデルを定義します。データベースにTaskテーブルを作成するためモデル名はTaskに設定しid, task, completed列を持つように定義します。idは自動で設定され、taskは文字列(String)、completedはBoolean(真偽値)を設定しています。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Task {
  id      Int      @id @default(autoincrement())
  task   String   
  completed    Boolean
}

利用するデータベースの設定とモデルの定義が完了したのでマイグレーションを実行します。マイグレーションを実行するとデータベースとテーブルが作成されます。

npx prisma migrate devコマンドを実行するとマイグレーションの名前をつけるように聞かれるので任意の名前を入力してください。ここではinitという名前をつけています。


 % npx prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

SQLite database dev.db created at file:./dev.db

✔ Enter a name for the new migration: … init
Applying migration `20221117061143_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20221117061143_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 2 packages, and audited 5 packages in 2s

found 0 vulnerabilities

✔ Generated Prisma Client (4.6.1 | library) to ./node_modules/@prisma/client in 150ms

コマンドを実行するとprismaフォルダにfile.dbファイルが作成されmigrationsフォルダが作成されます。その下には実行してタイムスタンプとコマンド時に入力した名前のタイムスタンプ_initフォルダが作成されその中のmigration.sqlにテーブルを作成するためのSQLが記述されています。

これでテーブルを作成することができたのでPrismaが持つPrisma Studioを起動します。Prisma Studioを利用することでGUIでデータベースの中身を確認することができデータの挿入や更新、削除も行うことができます。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

ブラウザからhttp://localhost:5555にアクセスしてTaskテーブルを選択して”Add Record”ボタンをクリックしてデータを挿入してください。

タスクデータの挿入
タスクデータの挿入

これでPrismaの設定は完了したのでExpressサーバの設定を行っていきます。

Expressのインストール

Expressサーバを構築するためにexpressとnodemonの2つのパッケージをインストールします。nodemonを利用することでExpress用のファイルを更新すると自動で再読み込みを行ってくれます。必須ではありません。


 % npm install express nodemon

インストールが完了したらindex.jsファイルを作成してExpressサーバを起動するためのコードを記述します。


const express = require('express');

const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

index.jsファイルが作成できたらnpx nodemon index.jsコマンドを実行してExpressサーバの起動を行います。


 % npx nodemon index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`

ポート番号3000で起動しているのでブラウザからhttp://localhost:3000にアクセスを行なってください。

ブラウザ上に”Hello World”が表示されたらExpressサーバは正常に起動しています。

Hello Worldの表示の確認
Hello Worldの表示の確認

起動が確認できたのでPrismaを経由してSQLiteデータベースのTaskテーブルからデータを取得します。


const express = require('express');
const { PrismaClient } = require('@prisma/client');

const app = express();
const port = 3000;

const prisma = new PrismaClient();

app.get('/', (req, res) => res.send('Hello World!'));

app.get('/tasks', async (req, res) => {
  const tasks = await prisma.task.findMany();
  return res.json(tasks);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

ルーティングの/tasksを追加しているのでブラウザから/tasksにアクセスしてタスク情報が表示されるか確認します。

ブラウザ上にタスク情報が表示されたらExpressサーバからPrismaを経由したSQLiteデータベースへの接続も正常に行われています。

タスク情報の表示
タスク情報の表示

データの読み込み

Vue.jsからExpressサーバのAPIの/tasksにアクセスしてタスク情報が取得できるか確認します。App.vueファイルにgetTasks関数の追加を行います。

getTasks関数ではfetch関数を利用してExpressサーバが起動しているhttp://localhost:3000/tasksにGETリクエストを送信しています。


<script setup>
import { ref } from 'vue';
const tasks = ref([]);
const task = ref('');

const getTasks = async () => {
  const response = await fetch('http://localhost:3000/tasks');
  const data = await response.json();
  tasks.value = data;
};

getTasks();

const addTask = () => {
  tasks.value.push({ id, task: task.value, complted: false });
  task.value = '';
  id++;
};

const deleteTask = (id) => {
  const index = tasks.value.findIndex((task) => task.id === id);
  tasks.value.splice(index, 1);
};
</script>

<template>
  <h1>Tasks</h1>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      :style="task.completed ? 'text-decoration:line-through' : ''"
    >
      <span><input type="checkbox" v-model="task.completed" /></span>
      <span>{{ task.task }}</span>
      <button @click="deleteTask(task.id)">削除</button>
    </li>
  </ul>
  <form @submit.prevent="addTask">
    <div>
      <input v-model="task" />
    </div>
    <div>
      <button type="submit">タスクを登録</button>
    </div>
  </form>
</template>

npm run devコマンドで開発サーバを起動するとデベロッパーツールにCORSに関連するエラーが発生しています。Vue.jsはlocalhost:5173で起動し、Expressサーバはlocalhost:3000で起動しておりポート番号が異なる(オリジン)ので通信を行うことができません。通信を許可するためにExpressサーバにcorsを設定する必要があります。

corsパッケージのインストールを行います。


 % npm install cors

インストールが完了したらcorsの設定を行います。


const express = require('express');
const { PrismaClient } = require('@prisma/client');

const app = express();
const port = 3000;
const cors = require('cors');

app.use(cors());

const prisma = new PrismaClient();

app.get('/', (req, res) => res.send('Hello World!'));

app.get('/tasks', async (req, res) => {
  const tasks = await prisma.task.findMany();
  return res.json(tasks);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));V

Vue.jsの開発サーバに接続しているブラウザをリロードするとタスク情報が表示されます。

Expressサーバを経由して取得したデータの表示
Expressサーバを経由して取得したデータの表示

Vue.jsからExpressサーバにGETリクエストを送信してSQLiteのデータベースに保存されているデータを取得し表示できるようになりました。

データの書き込み

データの書き込みを行うためaddTask関数を更新する前にExpressサーバ側でルーティングを追加しデータをデータベースに追加するための処理を追加する必要があります。


app.post('/tasks', async (req, res) => {
  const { task, completed } = req.body;
  const newTask = await prisma.task.create({
    data: {
      task,
      completed,
    },
  });
  return res.json(newTask);
});

送信されてきたJSONデータはreq.bodyの中に含まれていますがデータを取り出すためには下記の設定も追加する必要があります。


app.use(express.json());

Expressサーバ側のでデータ追加処理の追加後のコードは下記のようになります。


const express = require('express');
const { PrismaClient } = require('@prisma/client');

const app = express();
const port = 3000;
const cors = require('cors');

app.use(cors());
app.use(express.json());

const prisma = new PrismaClient();

app.get('/', (req, res) => res.send('Hello World!'));

app.get('/tasks', async (req, res) => {
  const tasks = await prisma.task.findMany();
  return res.json(tasks);
});

app.post('/tasks', async (req, res) => {
  const { task, completed } = req.body;
  const newTask = await prisma.task.create({
    data: {
      task,
      completed,
    },
  });
  return res.json(newTask);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Expressサーバの設定が完了したので次はVue.js側のaddTask関数を更新します。fetch関数を利用してPOSTリクエストをExpressサーバ側で追加したURLに対して送信しています。


const addTask = async () => {
  const response = await fetch('http://localhost:3000/tasks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ task: task.value, completed: false }),
  });
  const data = await response.json();
  tasks.value.push(data);
  task.value = '';
};

入力フォームにタスクを入力して”タスクを登録”ボタンをクリックするとブラウザ上に入力したタスクが表示されます。

Vue.jsからExpressサーバにPOSTリクエストを送信してSQLiteのデータベースに新たにデータを追加できるようになりました。

FirebaseやSupabaseではそれらが提供するライブラリを通してデータの取得や追加、更新、削除を行いましたがExpressサーバの場合はサーバが提供するURLに対してfetch関数でリクエストを送信することで処理を行うことができました。つまりExpressサーバでなくてもVue.jsがアクセス可能なURLとそのURLが受け取ったリクエストを処理できるサーバであればどの言語のバックエンドサーバでも構いません。またVue.jsからはバックエンドサーバが提供するURLに対してリクエストを送信するだけなのでどのデータベースをバックエンドサーバが利用しているのかも送信するデータと戻されるデータの形式さえわかれば関係はありません。

1つの文書の中で複数のデータベースの処理を確認することでそれぞれのサービス/サーバの違いの理解も深まったのではないでしょうか。