🍉

【Next.js × Firebase】DB接続からCRUD操作まで解説

2024/06/08に公開

はじめに

とにかくスピード重視で無料でDBを構築したい場合にfirebaseのfirestoreがおすすめです。
ドキュメントは充実していますが、毎回調べるのが面倒なので、これを見るだけでNext.jsの個人開発に爆速でDB構築ができるように記事にしました。

Firebaseの特徴

firebaseのDBであるfirestoreはNoSQLです。
JSON形式のドキュメントを使用してデータを保存します。

Firebaseセットアップ

firebaseはこちらから
https://firebase.google.com/?hl=ja

プロジェクトの作成

  1. プロジェクトを追加から手順にしたがって続行していきます。

  2. Googleアナリティクスの設定をして(私は無効にしました)「プロジェクトを作成」でプロジェクト作成完了です。※迷わないと思うので省略

アプリにFirebaseを追加する

  1. webを選択します。

  2. 手順に従ってアプリの登録をしていきます。※迷わないと思うので省略
    色々初期化処理のコードなどが出てきますが一旦無視して進めます。

Firestoreセットアップ

  1. Cloud Firestoreを選択>データベースの作成

  2. ロケーションの選択>テストモードor本番モードを選択>作成
    ロケーションはTokyo、本番モードを選択しました。

データの追加

DBにデータを追加していきます。
Firestoreは、リレーショナルデータベース(RDB)とは異なるデータ構造を持っており、ドキュメント指向のNoSQLデータベースです。以下のような構造を持ちます。

  • コレクション: 複数のドキュメントを含むグループ。
  • ドキュメント: ドキュメントはユニークなIDを持つ。
  • フィールド: ドキュメント内のデータ属性。文字列、数値、ブール値、配列、マップなどの異なるデータ型を含むことができる。

以下はtodoアプリに使う「tasks」コレクションの例です。

tasks (コレクション)
  |
  ├── task1 (ドキュメント)
  |     ├── id: "915202f2-ae92-42af-b313-b9ff6d45d31f" (フィールド)
  |     ├── title: "単体テスト" (フィールド)
  |     ├── description: "単体テストを実施する" (フィールド)
  |     ├── priority: "中" (フィールド)
  |     ├── status: "未着手" (フィールド)
  |     └── dueDate: 2024年6月4日 21:20:32 UTC+9 (フィールド)
  |
  └── task2 (ドキュメント)
        ├── id: "b49dd475-07ca-474e-b98b-682832ed1571" (フィールド)
        ├── title: "ログイン機能" (フィールド)
        ├── description: "ログイン機能の実装をする" (フィールド)
        ├── priority: "高" (フィールド)
        ├── status: "進行中" (フィールド)
        └── dueDate: 2024年6月2日 21:20:32 UTC+9 (フィールド)

データ追加手順

  1. コレクションを開始
    複数のドキュメントを含む「tasks」グループを作成します。

  2. ドキュメントの追加
    task1を追加していきます。ドキュメントIDは「task1」でもよいですが、自動IDからユニークなIDを生成できます。

ドキュメントを2つ追加して以下のようになりました。

DBにデータを追加したのでいよいよアプリ側と接続していきます。

Next.jsセットアップ

  1. 個人アプリを作成します。※DBと関係ない実装は省略
npx create-next-app@latest

Firebase + Firestore初期化処理

  1. インストール
npm install firebase
  1. 左上の設定>プロジェクトの設定

  2. こちらのページにあるconfig情報をコピーします。

Next.jsプロジェクトに以下のコードとconfig情報を貼り付けます。config情報はenvで管理しています。

src/lib/firebase/firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: process.env.FIREBASE_APIKEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STRAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MASSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Cloud Firestore and get a reference to the service
const db = getFirestore(app);

export default db;

以上で初期化処理は完了です。

CRUD操作

いよいよ作成したアプリからCRUD操作を行います。
前項でexportしたdbとfirebaseのメソッドが必要なのでimportします。

import db from "@/lib/firebase/firebase";
import {
  Timestamp,
  collection,
  deleteDoc,
  doc,
  getDocs,
  query,
  setDoc,
  updateDoc,
  where,
} from "firebase/firestore";

データ一覧の取得

collection(db, "tasks")の引数には初期化処理でexportしたdbと作成したコレクション名「tasks」を指定します。

const tasks = await getDocs(collection(db, "tasks")).then((snapshot) =>
  snapshot.docs.map((doc) => {
    return doc.data();
  })
);

コレクションごと取得すると複数のドキュメントが配列になって返ってきます。
tasksの中身

[
  {
    id: 'C3tewjWaNX6pps8QeT86',
    title: '単体テスト',
    description: '単体テストを実施する',
    priority: '中',
    status: '未着手',
    dueDate: Timestamp { seconds: 1717503632, nanoseconds: 342000000 },
  },
  {
    id: 'frnraJ7KHK4JJZdwJorO',
    title: 'ログイン機能',
    description: 'ログイン機能の実装をする',
    priority: '高',
    status: '進行中',
    dueDate: Timestamp { seconds: 1717977600, nanoseconds: 0 },
  }
]

条件付きデータの取得

フィールドのidが一致するドキュメントを取得します。

const col = collection(db, "tasks");
const q = query(col, where("id", "==", taskId));
const task = await getDocs(q).then((snapshot) => {
  return snapshot.docs[0].data();
});

該当のドキュメントが返ってきます。
taskの中身

{
    id: 'C3tewjWaNX6pps8QeT86',
    title: '単体テスト',
    description: '単体テストを実施する',
    priority: '中',
    status: '未着手',
    dueDate: Timestamp { seconds: 1717503632, nanoseconds: 342000000 },
 }

データの新規作成

doc(db, "tasks", task.id)の3つ目の引数に指定したものがドキュメントIDになります。指定しなければfirestore側で自動IDが生成され付与されます。

const docData = {
  id: task.id,
  title: task.title,
  description: task.description,
  dueDate: dateStringToTimestamp(task.dueDate), // Date -> timestamp自作関数
  priority: task.priority,
  status: task.status,
};

await setDoc(doc(db, "tasks", task.id), docData);

データの更新

doc(db, "tasks", taskId)の3つ目の引数には更新したいドキュメントのドキュメントIDを指定します。

const updateDocData = {
  id: taskId,
  title: task.title || null,
  description: task.description || null,
  dueDate: task.dueDate ? dateStringToTimestamp(task.dueDate) : null,
  priority: task.priority || null,
  status: task.status || null,
};
await updateDoc(doc(db, "tasks", taskId), updateDocData);

データの削除

doc(db, "tasks", taskId)の3つ目の引数には削除したいドキュメントのドキュメントIDを指定します。

await deleteDoc(doc(db, "tasks", taskId));

詳しくは公式ドキュメントを確認してください
https://firebase.google.com/docs/firestore?hl=ja

権限エラーの解消法

アプリ側からCRUD操作を行おうとするとこの権限周りのエラーが必ずでます。

Internal error: FirebaseError: Missing or insufficient permissions.

解消手順

  1. ルールの変更

  2. ルールを設定
    もともと全ての読み書きをif falseで許可していなかったものをサインインしているユーザーのみ許可に変更します。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
-      allow read, write: if false;
+      allow read, write: if request.auth != null;
    }
  }
}

参考:https://qiita.com/hibohiboo/items/7af59b6d0d9df98d96c4
しかしこれでも解消せず...
サインインというのがFirebase Authenticationを使ってのことらしく別途作業が必要そうだったので推奨されていない誰でも許可する設定にしました。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
-      allow read, write: if false;
+      allow read, write: if true;
    }
  }
}

この設定でエラーは解消されました。

まとめ

NoSQLだとDB設計に詳しくない人でも直感的に扱いやすく個人的に使いやすいと感じました。
しかも無料で使えるのでとりあえずミニアプリを作ってDB欲しいな...というときにちょうどいいです。(無料枠で使えるプロジェクト数の上限を超えてしまったのですが申請すればすぐに無料枠を増やしてもらえました。笑)
参考:
https://appdev-room.com/web-firebase-upper-limit
あとはfirestoreは階層構造が複雑でデータの取得が面倒でした。(型がよくわからない...)
しかし一度覚えてしまえばあとは簡単なのでぜひ使ってみてください^^

Discussion