【Firebase】Cloud Firestoreを使ってみる

Firebase

1. 概要

Cloud Firestoreを使い、簡単なCRUDを試してみます。

Cloud Firestore は、Firebase と Google Cloud からモバイル、ウェブ、サーバーを開発するための柔軟でスケーラブルなデータベースです。

コレクション全体またはネストされたサブコレクションを取得する必要なく、ドキュメント レベルでデータを取得するための浅いクエリを作成します。並べ替え、フィルタリング、および制限をクエリまたはカーソルに追加して、結果をページ分割します。更新が発生するたびにデータベース全体を取得せずに、アプリのデータを最新の状態に保つには、リアルタイム リスナーを追加します。リアルタイム リスナーをアプリに追加すると、クライアント アプリがリッスンしているデータが変更されるたびにデータ スナップショットで通知され、新しい変更のみが取得されます。

Firestore  |  Firebase (google.com)

データモデル
  • 敢えて言うなら
    • collection
      • RDB:Table
    • document
      • RDB:Record(行)
    • data
      • RDB:Column(列)

2. プロジェクトを作成

こちらを参考

3. Firestoreのセットアップ

こちらを参考

4. プロジェクトのロケーションを変更

こちらを参考

5. Functionsのセットアップ

こちらを参考

6. ソースコード

6-1. functions/src/common.ts

import * as functions from "firebase-functions";

export const f = functions.region("asia-northeast1");

6-2. functions/src/database.ts

import * as admin from "firebase-admin";

admin.initializeApp();

export const db = () => admin.firestore();

export const collectionNameUsers : string = 'users';

6-3. functions/src/types.ts

import * as admin from "firebase-admin";
import * as firestore from "firebase-admin/firestore";

export type Timestamp = admin.firestore.Timestamp | admin.firestore.FieldValue;

export type DocumentReference = firestore.DocumentReference;

export enum Sex {
    Male,
    Female
}

export enum DeleteTarget {
    Doc,
    Data
}

export enum DeleteFieldName {
    Age,
    Sex
}

6-4. functions/src/utils.ts

import * as firestore from "firebase-admin/firestore";

export const serverTimestamp = () => firestore.FieldValue.serverTimestamp();

export const fieldValueDelete = () => firestore.FieldValue.delete();

6-5. functions/src/params.ts

import * as firestore from "firebase-admin/firestore";
import * as types from "./types";

export type UserData = {
    name: {
      first: string,
      last: string
    },
    age: number,
    sex: types.Sex,
    updatedAt: types.Timestamp,
    createdAt?: types.Timestamp
};

export type UserDataForUpdate = {
    docId: string,
    merge: boolean,
    data: {
        name: {
            first: string,
            last: string
        },
        age: number,
        sex: types.Sex,
        updatedAt: types.Timestamp
    }
};

export type UserDataForDelete = {
    docId: string,
    deleteTarget: types.DeleteTarget,
    deleteFieldName: types.DeleteFieldName,
    updatedAt: types.Timestamp
}

export type DeleteField = {
    age?: number | firestore.FieldValue,
    sex?: types.Sex | firestore.FieldValue,
    updatedAt: types.Timestamp
};

6-6. functions/src/index.ts

import * as database from "./database";
import * as common from "./common";
import * as params from "./params";
import * as utils from "./utils";
import * as types from "./types";

export const getUsers = common.f.https.onRequest(async (request, response) => {
  const snapshot = await database.db().collection(database.collectionNameUsers).get();
  snapshot.forEach((user) => {
    console.log(user.id, ' => ', JSON.stringify(user.data()));
  });
  response.send('OK');
});

export const addUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userData : params.UserData = request.body;
    userData.updatedAt = utils.serverTimestamp();
    userData.createdAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc().set(userData);
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

export const setUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForUpdate : params.UserDataForUpdate = request.body;
    console.log('userDataForUpdate => ', userDataForUpdate);
    const docId : string = userDataForUpdate.docId;
    const userData : params.UserData = userDataForUpdate.data;
    userData.updatedAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc(docId).set(userData, {merge: userDataForUpdate.merge});
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

export const updateUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForUpdate : params.UserDataForUpdate = request.body;
    console.log('userDataForUpdate => ', userDataForUpdate);
    const docId : string = userDataForUpdate.docId;
    const userData : params.UserData = userDataForUpdate.data;
    userData.updatedAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc(docId).update(userData);
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

const deleteDoc = async (docRef: types.DocumentReference) => {
  try {
    await docRef.delete();
  } catch (e) {
    console.error(e);
  }
}

const deleteDataField = async (docRef : types.DocumentReference, deleteField : params.DeleteField) => {
  try {
    await docRef.update(deleteField);
  } catch (e) {
    console.error(e);
  }
}

export const deleteUserData = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForDelete : params.UserDataForDelete = request.body;
    console.log('userDataForDelete => ', userDataForDelete);
    const docRef = database.db().collection(database.collectionNameUsers).doc(userDataForDelete.docId);
    const deleteTarget : string = userDataForDelete.deleteTarget.toString();
    if (types.DeleteTarget[types.DeleteTarget.Doc].toString() === deleteTarget) {
      deleteDoc(docRef);
    } else {
      const deleteField: params.DeleteField = {
        updatedAt: utils.serverTimestamp()
      }
      if (types.DeleteFieldName[types.DeleteFieldName.Age].toString() === userDataForDelete.deleteFieldName.toString()) {
        deleteField.age = utils.fieldValueDelete();
      } else if (types.DeleteFieldName[types.DeleteFieldName.Sex].toString() === userDataForDelete.deleteFieldName.toString()) {
        deleteField.sex = utils.fieldValueDelete();
      } else {
        console.log('Nothing do.');
      }
      deleteDataField(docRef, deleteField);
    }
    response.send('OK');
  }
});

7. エミュレータ スイートのセットアップ

7-1. 初期化

7-2. functions/package.json

{
  "name": "functions",
  "scripts": {
    "lint": "eslint --ext .js,.ts .",
    "build": "tsc",
    "build:watch": "tsc --watch",
    "serve": "npm run build && firebase emulators:start",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "18"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^11.5.0",
    "firebase-functions": "^4.2.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.48.2",
    "@typescript-eslint/parser": "^5.48.2",
    "eslint": "^8.32.0",
    "eslint-config-google": "^0.14.0",
    "eslint-plugin-import": "^2.27.5",
    "firebase-functions-test": "^3.0.0",
    "typescript": "^4.9.4"
  },
  "private": true
}

7-3. エミュレータを起動

cd functions
npm run serve
> serve
> npm run build && firebase emulators:start


> build
> tsc

i  emulators: Starting emulators: functions, firestore, storage
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, hosting, pubsub
✔  functions: Using node@18 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
✔  firestore: Firestore Emulator UI websocket is running on 9150.
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/home/sondon/dev/wsl/gcp/firebase/sample003/functions" for Cloud Functions...
✔  functions: Loaded functions definitions from source: getUsers, addUser, setUser, updateUser, deleteUserData.
✔  functions[asia-northeast1-getUsers]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/getUsers).
✔  functions[asia-northeast1-addUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser).
✔  functions[asia-northeast1-setUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/setUser).
✔  functions[asia-northeast1-updateUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/updateUser).
✔  functions[asia-northeast1-deleteUserData]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/deleteUserData).

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000/               │
└─────────────────────────────────────────────────────────────┘

┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├───────────┼────────────────┼─────────────────────────────────┤
│ Storage   │ 127.0.0.1:9199 │ http://127.0.0.1:4000/storage   │
└───────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500, 9150

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

8. データの登録

8-1. 登録

curl -X POST -H "Content-Type: application/json" -d '{"name": {"first":"taro","last":"tanaka"},"age":21,"sex": "male"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser
curl -X POST -H "Content-Type: application/json" -d '{"name": {"first":"mie","last":"nakata"},"age":22,"sex": "female"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser
curl -X POST -H "Content-Type: application/json" -d '{"name": {"first":"jun","last":"sato"},"age":23,"sex": "male"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser

8-2. 確認

curl http://127.0.0.1:5001/isub-sample-003/asia-northeast1/getUsers
i  functions: Beginning execution of "getUsers"
>  KzfWnGcrsIcofrRQedHH  =>  {"sex":"male","name":{"last":"tanaka","first":"taro"},"age":21,"createdAt":{"_seconds":1674893383,"_nanoseconds":14000000},"updatedAt":{"_seconds":1674893383,"_nanoseconds":14000000}}
>  eUX8hr7ckjssXcRjjcFE  =>  {"sex":"male","name":{"last":"sato","first":"jun"},"age":23,"createdAt":{"_seconds":1674893395,"_nanoseconds":359000000},"updatedAt":{"_seconds":1674893395,"_nanoseconds":359000000}}
>  gyQvwwARmDnzcKCqaM1e  =>  {"sex":"female","name":{"last":"nakata","first":"mie"},"age":22,"createdAt":{"_seconds":1674893389,"_nanoseconds":241000000},"updatedAt":{"_seconds":1674893389,"_nanoseconds":241000000}}
i  functions: Finished "getUsers" in 242.25937ms

9. 上書き

9-1. merge:true

curl -X POST -H "Content-Type: application/json" -d '{"docId":"KzfWnGcrsIcofrRQedHH","merge":true,"data":{"name": {"first":"jiro"}}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/setUser
  • 「taro」→「jiro」

9-2. merge:false

curl -X POST -H "Content-Type: application/json" -d '{"docId":"KzfWnGcrsIcofrRQedHH","merge":false,"data":{"name": {"first":"jiro"}}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/setUser
  • 指定されてる項目のみ存在
  • 指定されてない項目はなくなる
  • 「merge:false」がデフォルトのため、注意が必要

10. 更新

10-1. 更新

curl -X POST -H "Content-Type: application/json" -d '{"docId":"gyQvwwARmDnzcKCqaM1e","data":{"age":20,"sex": "female"}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/updateUser
  • 「22」→「20」

10-2. 更新(ネストされたオブジェクトのフィールド)

curl -X POST -H "Content-Type: application/json" -d '{"docId":"gyQvwwARmDnzcKCqaM1e","data":{"name": {"first":"miza"}}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/updateUser
  • ネストされたフィールドをドット表記なしで更新すると、マップフィールド全体が上書きされる
    • ネストされたオブジェクトの指定されてる項目は存在
    • ネストされたオブジェクトの指定されてない項目は無くなる

10-3. 更新(ネストされたオブジェクトのフィールド)

curl -X POST -H "Content-Type: application/json" -d '{"docId":"eUX8hr7ckjssXcRjjcFE","data":{"name.first": "jiro"}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/updateUser
  • ネストされたフィールドをドット表記で更新すると、他のネストされたフィールドを上書きすることなく、単一のネストされたフィールドを更新できる
    • 「name.first」
      • 「jun」→「jiro」

11. 削除

11-1. フィールドの削除

curl -X POST -H "Content-Type: application/json" -d '{"docId":"eUX8hr7ckjssXcRjjcFE","deleteTarget":"Data","deleteFieldName":"Sex"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/deleteUserData

  • 「sex」項目が存在しない

11-2. ドキュメントの削除

curl -X POST -H "Content-Type: application/json" -d '{"docId":"eUX8hr7ckjssXcRjjcFE","deleteTarget":"Doc"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/deleteUserData

  • 対象データが存在しない

curl http://127.0.0.1:5001/isub-sample-003/asia-northeast1/getUsers
i  functions: Beginning execution of "getUsers"
>  KzfWnGcrsIcofrRQedHH  =>  {"name":{"first":"jiro"},"updatedAt":{"_seconds":1674893927,"_nanoseconds":399000000}}
>  gyQvwwARmDnzcKCqaM1e  =>  {"sex":"female","createdAt":{"_seconds":1674893389,"_nanoseconds":241000000},"age":20,"name":{"first":"miza"},"updatedAt":{"_seconds":1674894752,"_nanoseconds":268000000}}
i  functions: Finished "getUsers" in 11.944577ms

12. 備考

シンプルな例で、データの登録・取得・更新・削除ができる事を確認しました。

次回はもう少し深堀できたらと思います。

13. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. Firebase

    【Firebase】Cloud Functionsを使ってみる

  2. flutter

    【Flutter】Cloud Firestoreと連携

  3. Firebase

    【Firebase】Local Emulator Suiteを起動する…

  4. Firebase

    【Firebase】CloudFirestoreトリガーを使ってみる

  5. Firebase

    【Firebase】Firestore Security Rulesを…

  6. flutter

    【Flutter】Firebase Dynamic Linksを使う

最近の記事

  1. Node.js
  2. AWS
  3. AWS

制作実績一覧

  1. Checkeys