🔥

Firebase入門 FireStore編(v9) ※編集中

2021/11/28に公開

参照(公式)

https://firebase.google.com/docs/firestore/manage-data/add-data?authuser=0#web-version-9_1

【はじめに】

公式のドキュメントを読んだ内容をまとめる。コピペして利用したいので説明とコードを合わせて記載する。

FireStoreを利用するにはFirebaseのセットアップとデータベースの作成を事前に行う必要がある。
セットアップは以下の記事参照。
https://zenn.dev/nash/articles/131441b8999209

ほかにも、データベースへのアクセス制限ルールなどを定義できるのだが、まだよくわかっていないので学習が完了したら執筆する予定。

【FireStoreの基本知識】

ロケーション

firestoreページにてデータベースを作成するとロケーションを選択できる。
日本に近ければどこでもよいが公式から以下が提供されているので参考にする。

私は東京での利用を想定して「asia-northeast1」に設定。

・公式ページ
https://firebase.google.com/docs/functions/locations?hl=ja

データ構想

この記事では「コレクション、ドキュメント、データ」という3つのワードが頻繁に出てくる。これは「NoSQL ドキュメント指向データベース」で使われる概念。

次項にて簡単に説明する。

https://firebase.google.com/docs/firestore/data-model?hl=ja
データ構造の詳細は上記公式ページからも確認できる。

コレクション

コレクションにはドキュメントを保存することができる。コレクションの中にはコレクションを保存することはできない。それぞれのコレクションはIDを持つ。
コレクションに保存するドキュメントはそれぞれユニークなIDを持つ必要がある。Firestore上でドキュメントを作成すると次のように自分で決めるか自動生成するか選択できる。

※コレクションが異なればドキュメントIDの重複はしてもOK。

ドキュメントは必ずコレクション内に保存される。ドキュメントが作成されるときにコレクションも作られる。また、コレクション内に1件もドキュメントがないときコレクションは削除される。したがって、開発者が明示的にコレクションの作成・削除は行わなくてよい。

ドキュメント

コレクションの中に保存される。JSONに似た構造のデータ。

データ

ドキュメントに保存される。key: valueの値。

サブコレクション

ドキュメントに保存されるコレクション。
必ず、次のような構造になる。
コレクション>ドキュメント>サブコレクション>ドキュメント>サブコレクション>・・・

リファレンス

ドキュメントやコレクションがFirestoreのどこに保存されているのかを示すパス。
Firestoreから提供されるデータ操作関数にリファレンスを渡すことで、目的のデータを見つけCRUD操作をするイメージ。

コレクショングループ

同一のIDを持つコレクションを1つのコレクションとする機能。

FireStoreへの接続情報を取得

FireStoreとデータのやり取りをするには、Firestoreへの接続情報を取得する必要がある。FireStoreへの接続情報の取得にはgetFirestoreを利用する。getFirestoreの戻り値(接続情報)を変数dbに格納し、FireStore用のapiに変数dbを渡すことでデータのCRUD操作を実現する。

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

export const app = initializeApp({
  apiKey: process.env.REACT_APP_FIREBASE_APIKEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_MESSAGING_APP_ID,
});
// 接続情報を変数dbに格納
export const db = getFirestore(app);

【リファレンス(データの参照情報)の取得】

データのCRUDするには、「どこどこのデータ」というデータの配置場所の情報が必要。
本項では参照情報の取得方法を記載する。
※データの参照情報は次項以降で利用するので、本項は次項以降を読んでからだと理解が捗る。

コレクションへの参照を取得

// dbをimportする(この後の例では省略する)
import { db } from "./firebaseConfig";
// FireStoreのAPI(この後の例では省略する)
import {collection, doc} from "firebase/firestore";

const colRef = collection(db, コレクション名); // 例
const colRef = collection(db, "cities"); // 実際の記述

単一documentへの参照を取得

const docRef = doc(コレクションへの参照, ドキュメント名); // 例
const docRef = doc(colRef, "LA"); // 実際の記述
const docRef = doc(db, "cities", "LA"); // 引数を3つにすることも可能

自動生成ドキュメントIDを持つドキュメントへの参照を取得

const colRef = collection(db, "cities");
const newCityRef = doc(colRef); // ドキュメント名の引数がないので、ランダムなIDが振られる

【作成・更新】

ドキュメントの作成と更新(setDoc)

setDoc関数ではドキュメントリファレンスを利用する。
ドキュメントIDを指定してデータを追加する。

指定したドキュメント名がFireStoreに存在しない場合は指定したドキュメント名で新たなドキュメントを作成する。すでにあればデータは上書きされる。

※基本的には必要なときにドキュメント名指定でドキュメントを作成し、それ以外は次項で説明するランダムIDでドキュメントを作成する。

const docRef = doc(db, "cities", "LA");
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA",
}

await setDoc(ドキュメントへの参照, data); // 例
await setDoc(docRef, data); // 実際のコード

ランダムなIDが付与されたドキュメントの作成(addDoc)

addDoc関数ではコレクションリファレンスを利用する。

下記の例の通りドキュメントIDを指定しないので、FireStoreが自動的に一意のドキュメントIDを割り振ってドキュメントを作成する。
※基本的にランダムなIDでドキュメントを作り、必要な時のみドキュメントIDを指定して作成する。

// 準備
const colRef = collection(db, "cities");
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA"
};
// addDoc
await addDoc(コレクションへの参照, data); // 例
await addDoc(colRef, data); // 実際のコード

ランダムなIDが付与されたドキュメントの更新(実践的なsetDoc)

「ランダムなIDが付与されたドキュメントの作成(addDoc)」では、ドキュメントとデータを同時に作成するため、ドキュメントへのリファレンスが存在しない。つまり、ドキュメントの作成後に作成したドキュメントを更新することができない。
以下のようにドキュメントへの参照をnewCityRefという変数に入れて持ち回せば、setDocしたあとでも編集が可能になる。

const newCityRef = doc(collection(db, "cities"));
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA",
};
await setDoc(newCityRef, data);

ドキュメントの一部のデータを更新・作成(updateDoc)

フィールドの更新。更新は単一のドキュメントでしか行えないため、クエリで複数ドキュメントを取得したあと、ループを回して同じ個所を更新する必要がある。

const newCityRef = doc(collection(db, "cities"));
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA",
};
await setDoc(newCityRef, data);
await updateDoc(newCityRef, {
  country: "Tokyo", // USAからTokyoに更新
});

新たにcapitalフィールドを追加している。

const newCityRef = doc(collection(db, "cities"));
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA",
  // capitalはない
};
await setDoc(newCityRef, data);
await updateDoc(newCityRef, {
  capital: true, // capitalフィールドを新規作成
});

ネストされたフィールドの更新

ネストされたフィールドは書き方にルールがある。そのルールで書かないと意図しないデータが更新されてしまうので注意。

クォーテーションでkey名を囲みドットフィールド名とすることでネストされたフィールドにアクセスできる。

const newUserRef = doc(collection(db, "users"));
const data = {
  name: "Frank",
  favorites: { food: "Pizza", color: "Blue", subject: "recess" },
  age: 12,
};
await setDoc(newUserRef, data);
await updateDoc(newUserRef, {
  age: 13,
  // クォーテーション
  "favorites.color": "Red",
});

上記コードのよう書かないと、例えばfavoritesを上書きするので注意。以下の様な状態。

const newUserRef = doc(collection(db, "users"));
const data = {
  name: "Frank",
  favorites: { food: "Pizza", color: "Blue", subject: "recess" },
  age: 12,
};
await setDoc(newUserRef, data);
await updateDoc(newUserRef, {
  age: 13,
  "favorites": {
    color:"Red" // ドットつなぎでアクセスしていない。。。
  },
});

// 更新後は次のようなデータ構造になってしまう!!
{
  name: "Frank",
  favorites: {
    // color以外のプロパティがなくなっている。
    color:"Red"
  }
  age: 13,
};

サーバー更新受信時刻を追跡するタイムスタンプを設定

serverTimestamp()の部分。

const newCityRef = doc(collection(db, "cities"));
const data = {
  name: "Los Angeles",
  state: "CA",
  country: "USA",
};
await setDoc(newCityRef, data);
await updateDoc(newCityRef, {
  timestamp: serverTimestamp(),

日付の操作に関しては以下の記事を参照
https://zenn.dev/nash/articles/124b1c967387bd

整理

addDocはドキュメントの作成。
setDocはドキュメントの作成と更新。
updateDocはドキュメントのデータを作成と更新。

addDocとsetDocは等価な結果となるためFirebase公式では「完全に同等なので、どちらでも使いやすい方を使うことができます。」と記載がある。

一般的なDB設計の考え方ではIDはランダムが良いため、addDocかsetDocドキュメントID未指定で作成する。必要に迫られる場面はあるのでそのときはドキュメントIDを指定する。

IDはランダムで振られて基本的にかぶることはない。(ただし、IDがかぶる可能性は0ではない。被った一方のIDを再採番するという設定をすることで回避は可能。方法は調査中)

コード書き方には注意が必要。setDocは作成・更新が可能なため意図しない更新をしてしまう可能性がある。そのため1つのコードの中で、作成のみの場合はaddを、更新のみはupdateを、作成・更新があればsetDocを利用して、setDocを利用する場合は更新に注意して書く必要がある。

ただし、煩雑となるため分けて書くのが良さそう。
addDoc()の戻り値.idとすればドキュメントIDは取得できるので、それを次の処理に渡していくということはできそう。

【参照】

単一ドキュメントを参照

ドキュメントへのリファレンスを取得してgetDoc関数に渡すことで、単一ドキュメントのデータを参照できる。
getDocの返り値はDocumentSnapshotオブジェクト。DocumentSnapshotオブジェクトはexistsプロパティを持っており、existsを使ってnullチェックができる。データが存在すればtrue。取得したデータへのアクセスはdataメソッドを利用する。

const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);

if (docSnap.exists()) {
  console.log("Document data:", docSnap.data());
} else {
  // doc.data() がUndefinedの場合の処理
  console.log("No such document!");
}

複数ドキュメントへの参照を取得する

複数取得の際はquery()というプロパティを利用する。query()にはどのドキュメントを取得するかという条件を定義する(SQL見たいな感じ)。
query()の第一引数にコレクションリファレンスを指定して、第二引数には取得するドキュメントの条件を定義する。条件の定義にはwhereを利用する。
query()によって作られたクエリをgetDocs()に渡すことで条件にあったドキュメントが取得できる。

const q = query(collection(db, "cities"), where("capital", "==", true));

const querySnapshot = await getDocs(q);

querySnapshot.forEach((doc) => {
  console.log(doc.id, " => ", doc.data());
});

全件取得

ドキュメントに対してwhere()で条件を指定しない。条件指定しないので何も取得しないように感じるが逆で、何も指定しないのだからすべて取得することになる。
getDocs()にcollectionリファレンスを渡せばOK.

const querySnapshot = await getDocs(collection(db, "cities"));

querySnapshot.forEach((doc) => {
  console.log(doc.id, " => ", doc.data());
});

おまけ:【console.log()でquerySnapshotの中身を確認する方法】
querySnapshot.docsが取得してきたドキュメントを配列で持っている。
querySnapshot.docs[0].idとすればドキュメント名が確認できる。
querySnapshot.docs[0].[[Prototype]].data()を利用してドキュメントのデータを取得する。

【削除】

単一のドキュメントの削除

// 単一のドキュメントリファレンスを取得
const docRef = doc(db, "cities", "DC");
// 削除
await deleteDoc(docRef);

Discussion