日々ろぐ

人に優しく٩( 'ω' )و

Vue.js × Firestore のCRUDを改めて調べてみた

作ったもの

Markdownのテキスト管理画面みたいなもの。
FirestoreのCRULを確認がてら、Markdownの表示確認もしたかったので一緒にして作ってみた。

github.com

Firestoreのデータモデル

※詳しくは公式ドキュメントにまとめられているので、そちらを見たほうが確実です。

Firestoreのデータモデル、自分なりの解釈をざっくりまとめてみる。

ドキュメント

データの単位。RDBでいうレコードに当たるもの。
各ドキュメントはidにより一意に識別される。
RBDとは異なり、ドキュメント間で保持する項目が異なってもよい。
また、階層化データを保持することも可能で、ドキュメント内に後述のコレクションを保持することも可能。(サブコレクション)

コレクション

ドキュメントのまとめる階層(フォルダ的なもの)。
RDBでいうテーブルに当たるもの。
ドキュメントの項目にも書いたように、コレクション内のドキュメントが保持する項目は一致しなくともよい。
なので、同コレクション内にあるドキュメントでも保持する項目に差異があることも可能。

リファレンス

コレクション内のドキュメントを参照するための場所を示したオブジェクト。(パスのようなもの)
ただし、そこにデータが存在しなくともリファレンスを作成することは可能。

FirestoreのCRUD

Javascript(Vue.js)でFiresotreへCRUDする例をまとめてみました。
環境はこんな感じです

node v10.15.3
vue-cli v3.8.2
firebase-tools v7.0.0

CREATE

コレクションのadd()メソッドを利用して、ドキュメントを新規追加する例です。
※フォームのデータをそのまま登録してるので、必要に応じてバリデーションは必要。
この例ではcontentsコレクションにドキュメントを追加していますが、その際に付与されるIDはFirestoreにて自動的に生成されます。
生成されたデータはコンソールで確認することができます。

また、contentsコレクションが存在しない場合でも、このadd()メソッドで登録する際に自動的にコレクションの作成まで行われます。

<template>
  <div class="Add">
    <input v-model="formData.title" placeholder="title">
    <input v-model="formData.category" placeholder="category">
    <input v-model="formData.tags" placeholder="tags">
    <textarea v-model="formData.content" placeholder="content"></textarea>
    <button @click="create">registration</button>
  </div>
</template>

<script>
    ~中略~
export default {
    ~中略~
  methods: {
    create () {
      // Firebaseへデータを登録する
      firebase.collection('contents').add(this.formData).then((docRef) => {
        alert('データを登録しました。ID: ' + docRef.id)
      }).catch((error) => {
        console.error('Error adding document: ', error)
      })
    }
  }
}

READ

データを取得するには、doc()メソッドを使用してリファレンスを取得します。
リファレンスに対して、get()メソッドを利用してドキュメントを取得します。
ただし、先述の通りリファレンスそのものはドキュメントが存在しなくとも作成することができるので、get()メソッドで取得したデータが存在しているかのチェックをexists()メソッドで行っています。
また、取得したドキュメントのcontentの項目には改行コードが含まれていますが、Firestoreのテキスト型は改行コードをエスケープしているため、改行文字の置換処理を行っています。

export default {
    ~中略~
  methods: {
    getText () {
      // Firebaseからデータを取得する
      var docRef = firebase.collection('contents').doc(this.$route.params.id)
      var ret = ''

      // FireStoreのデータ取得は非同期
      docRef.get().then((doc) => {
        if (doc.exists) {
          // 改行文字の置換
          ret = doc.data().content.replace(/\\n/g, '\n')
        } else {
          ret = 'データがありませんでした'
        }
      })
    }
  }
}
</script>

上記はIDを指定して単一のドキュメントを取得する処理でした。
コレクション内の全データを取得する場合は、コレクションに対してget()メソッドを利用して取得することができます。

firebase.collection('contents').get().then((docs) => {
  docs.foreach((doc) => {
    // doc.data() に対する操作
  })
})

コレクション内のデータを取得する際に、条件を付与する場合は.whereメソッドを利用します。
例えば、カテゴリーが「Dev」のデータのみ取得する場合は以下のような実装になります。

firebase.collection('contents').where("category", "==", "Dev").get().then((docs) => {
  docs.foreach((doc) => {
    // doc.data() に対する操作
  })
})

複数の条件(AND条件)を付ける場合は、where()を連結して記述することで実現できます。
ただし、FirestoreはOR条件をサポートしていません。なので、別々にドキュメントを取得しておいて、アプリケーション側でデータの整形をする必要があります。

firebase.google.com

また、Like検索も同様にサポートはされていないようですが、以下のnoteで同等の処理を実現できるようです。

note.mu

UPDATE

データの更新にはset()メソッドを利用します。
リファレンスに対してデータをセットすることで、対象ドキュメントの更新処理を行います。

export default {
    ~中略~
  methods: {
    update () {
      var docRef = firebase.collection('contents').doc(this.$route.params.id)
      
      // Firebaseのデータを更新する
      docRef.set(this.formData).then(() => {
        alert('データを更新しました。')
      }).catch((error) => {
        console.error('Error update document: ', error)
      })
    }
  }
}
</script>

ただし、リファレンスで指定した対象のドキュメントがない場合、新たにドキュメントが作成されます。(add()と同等の動き)

DELETE

データの削除にはdelete()メソッドを利用します。

export default {
    ~中略~
  methods: {
    update () {
      var docRef = firebase.collection('contents').doc(this.$route.params.id)
      
      // Firebaseのデータを削除する
      docRef.delete().then(() => {
        alert('データを削除しました。')
      }).catch((error) => {
        console.error('Error delete document: ', error)
      })
    }
  }
}
</script>

また、ドキュメント内の特定のフィールドのみ削除したい場合、リファレンスに対してFieldValue.delete()メソッドを利用して実現することも可能です。

注意として、データの削除はドキュメント単位で行う必要があります。
※SELECTのようにコレクションに対してWhere条件の指定ができない。

stackoverflow.com

なので、全研データを削除する場合は、ドキュメントのリストを取得してから一件一件delete()を繰り返す必要があります。
なお、コレクションを削除したい場合は、コレクション内のドキュメントをすべて削除する必要があります。
※クライアントからのコレクション削除は非推奨です。

firebase.google.com

まとめ

改めて振り返ると、条件指定の制限や、削除の難しさなどの面で若干の不自由があります。
また、データが増えるごとにダイレクトにパフォーマンスに影響が出るので、不要なデータの持ち方などのデータ構造設計が難しいですね。
(結構これで苦戦してるって話を聞く気がする)

データモデルに関していうと、小ぶりなサービスであればなんとかなるのかな、という印象です。
ですが、規模が大きくなると結構しんどいんじゃないかなーと思ってます。