Firestoreのデータ操作(取得, 追加, 更新, 削除)

Cloud Firestore SDKを利用して、Firestoreのデータ操作方法を確認します。「ドキュメントの追加, 更新, 取得, 削除」「ページング処理」「トランザクション」「一括書き込み」といった操作方法を取り上げます。

なお、データベースの作成などは下記ページで実行済みです。
Firebase|【入門】Firestoreの使い方 - わくわくBank

ドキュメントを追加

新規作成( 自動的にID生成 )
 - CollectionReference.add(docData)

CollectionReferenceのaddメソッド を利用してドキュメントを追加します。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const userRef = await db.collection('users').add({
      name: {
        first: 'tarou',
        last: 'yamada',
      },
      score: 80,
      birthday: firebase.firestore.Timestamp.fromDate(new Date(1980, 10, 15)),
      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
      updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
    })

    const userDoc = await userRef.get()
    console.log(userDoc.data())
    // 出力例
    // { birthday: Timestamp { seconds: 343062000, nanoseconds: 0 },
    //   createdAt: Timestamp { seconds: 1571747519, nanoseconds: 521000000 },
    //   name: { first: 'tarou', last: 'yamada' },
    //   score: 80,
    //   updatedAt: Timestamp { seconds: 1571747519, nanoseconds: 521000000 } }
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()

以下のように、ドキュメントIDが自動採番されました。
723-firebase-firestore-query_add_01.png

新規作成( 自動的にID生成 )
 - CollectionReference.doc()
 - DocumentReference.set(docData)

DocumentReferenceのsetメソッド を利用してドキュメントを追加することもできます。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const userRef = db.collection('users').doc()
    await userRef.set({
      name: {
        first: 'tarou',
        last: 'yamada',
      },
      score: 80,
      birthday: firebase.firestore.Timestamp.fromDate(new Date(1980, 10, 15)),
      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
      updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
    })
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()

新規作成( ID指定 )
 - CollectionReference.doc(docId)
 - DocumentReference.set(docData)

CollectionReferenceのdocメソッド の引数で、ドキュメントIDを指定することもできます。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const userRef = db.collection('users').doc('abcdefg')
    await userRef.set({
      name1: 'xxxxx',
      name2: 'yyyyy',
    })
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()

以下のように指定したドキュメントIDが設定されました。

723-firebase-firestore-query_set_create_01.png

なお、すでに指定ドキュメントが存在する場合の挙動は setメソッド の第2引数で調整できます。

// set()に渡したデータのみを更新する
// → name2は残ったままで、name1のみが更新される
await userRef.set({ name1: 'abc' }, { merge: true })

// ドキュメント内の全てのデータが新しいデータで上書きされる
// → name2が無くなる
await userRef.set({ name1: 'abc' })
await userRef.set({ name1: 'abc' }, { merge: false })

ドキュメントを更新

下記ドキュメントを更新してみます。

723-firebase-firestore-query_update_01.png

更新
 - CollectionReference.doc(docId)
 - DocumentReference.update(docData)

DocumentReferenceのupdateメソッド を利用してドキュメント更新します。

const userRef = db.collection('users').doc('4eADonIyVHL8bdISoN5T')
await userRef.update({
  score: 80,
  updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
})

以下のように更新されました。

723-firebase-firestore-query_update_02.png

もし、ドキュメントに存在しないデータを指定した場合、データが追記されます。

mapデータの更新
( ドット表記を使用する )

mapデータの一部データを更新する場合、ドット表記 を利用します。

const userRef = db.collection('users').doc('4eADonIyVHL8bdISoN5T')
await userRef.update({
  'name.last': 'suzuki',
  updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
})

以下のように、name.last が更新されました。

723-firebase-firestore-query_update_03.png

mapデータの更新
( ドット表記を使用しない )

ドット表記を使用しない場合、mapデータの全データが更新されます。

const userRef = db.collection('users').doc('4eADonIyVHL8bdISoN5T')
await userRef.update({
  name: {
    last: 'tanaka',
  },
  updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
})

name.first の指定がないため、以下のように name.last だけになりました。

723-firebase-firestore-query_update_04.png

ドキュメントを取得

前準備

前準備として以下処理を実行しておきます。

await db.collection('posts').add({ title: 'title1', body: 'body1', likesCount: 50, category: 'life' })
await db.collection('posts').add({ title: 'title2', body: 'body2', likesCount: 20, category: 'news' })
await db.collection('posts').add({ title: 'title3', body: 'body3', likesCount: 40, category: 'news' })
await db.collection('posts').add({ title: 'title4', body: 'body4', likesCount: 28, category: 'life' })
await db.collection('posts').add({ title: 'title5', body: 'body5', likesCount: 39, category: 'music' })
await db.collection('posts').add({ title: 'title6', body: 'body6', likesCount: 42, category: 'science' })
await db.collection('posts').add({ title: 'title7', body: 'body7', likesCount: 29, category: 'science' })
await db.collection('posts').add({ title: 'title8', body: 'body8', likesCount: 47, category: 'life' })
await db.collection('posts').add({ title: 'title9', body: 'body9', likesCount: 36, category: 'news' })
await db.collection('posts').add({ title: 'title10', body: 'body10', likesCount: 39, category: 'life' })
await db.collection('posts').add({ title: 'title11', body: 'body11', likesCount: 21, category: 'music' })
await db.collection('posts').add({ title: 'title12', body: 'body12', likesCount: 43, category: 'science' })
723-firebase-firestore-query_read_pre2.png

postsコレクションに12ドキュメント登録されました。

単一ドキュメントを取得
 - CollectionReference.doc(docId)
 - DocumentReference.get()
 - ( 戻り値: DocumentSnapshot )

postsコレクションの中から ドキュメントID=5H7wnLfbnupsWuFnsST1 のドキュメントを取得します。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const postRef = db.collection('posts').doc('AOPFbwazI7HoE2ofXm5Q')
    const postDoc = await postRef.get() // firebase.firestore.DocumentSnapshotのインスタンスを取得
    if (postDoc.exists) {
      console.log(postDoc.id)
      console.log(postDoc.data())
      console.log(postDoc.get('title'))
    } else {
      console.log('No such document!')
    }

    await db.app.delete()
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()
AOPFbwazI7HoE2ofXm5Q
{ body: 'body6',
  category: 'science',
  likesCount: 42,
  title: 'title6' }
title6

複数ドキュメントを取得
 - CollectionReference.get()
 - ( 戻り値: QuerySnapshot )

postsコレクションの全ドキュメントを取得します。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const querySnapshot = await db.collection('posts')
      .get() // firebase.firestore.QuerySnapshotのインスタンスを取得
    console.log(querySnapshot.size)
    console.log(querySnapshot.empty)
    console.log(querySnapshot.docs.map(postDoc => postDoc.id))
    querySnapshot.forEach((postDoc) => {
      console.log(postDoc.id, ' => ', JSON.stringify(postDoc.data()))
    })

    await db.app.delete()
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()
12
false
[ 'AOPFbwazI7HoE2ofXm5Q',
  'CAy51oSyQVpoayfY54k9',
  'OYCZTAtR7MNFnP1iMVX4',
  'ThJZPUBdj95ozjfqgLJh',
  'VvuGkWmH9Hkd3LorLUaI',
  'dEEUHM65w1B1CNinrZPF',
  'jaovhErVfrFudbkVEd8y',
  'rHdnl1vDRjeXWD97paVc',
  'u0FMUCZJeUMljyt4QJwO',
  'u4g5Q58lbOROMy3TDvrF',
  'vDJCwIo2vRpHf2jTkgaU',
  'yu0IIVoaxzDtEEJ5vCBV' ]
AOPFbwazI7HoE2ofXm5Q  =>  {"body":"body6","category":"science","likesCount":42,"title":"title6"}
CAy51oSyQVpoayfY54k9  =>  {"body":"body5","category":"music","likesCount":39,"title":"title5"}
OYCZTAtR7MNFnP1iMVX4  =>  {"body":"body3","category":"news","likesCount":40,"title":"title3"}
ThJZPUBdj95ozjfqgLJh  =>  {"body":"body9","category":"news","likesCount":36,"title":"title9"}
VvuGkWmH9Hkd3LorLUaI  =>  {"body":"body2","category":"news","likesCount":20,"title":"title2"}
dEEUHM65w1B1CNinrZPF  =>  {"body":"body11","category":"music","likesCount":21,"title":"title11"}
jaovhErVfrFudbkVEd8y  =>  {"body":"body7","category":"science","likesCount":29,"title":"title7"}
rHdnl1vDRjeXWD97paVc  =>  {"body":"body1","category":"life","likesCount":50,"title":"title1"}
u0FMUCZJeUMljyt4QJwO  =>  {"body":"body12","category":"science","likesCount":43,"title":"title12"}
u4g5Q58lbOROMy3TDvrF  =>  {"body":"body10","category":"life","likesCount":39,"title":"title10"}
vDJCwIo2vRpHf2jTkgaU  =>  {"body":"body4","category":"life","likesCount":28,"title":"title4"}
yu0IIVoaxzDtEEJ5vCBV  =>  {"body":"body8","category":"life","likesCount":47,"title":"title8"}

条件指定
 - CollectionReference.where(条件).get()

CollectionReferenceのwhereメソッド で条件( < <= == > >= array-contains )指定できます。

categorylife であるドキュメントを取得してみます。

const querySnapshot = await db.collection('posts')
  .where('category', '==', 'life')
  .get()
console.log(querySnapshot.size)
console.log(querySnapshot.empty)
querySnapshot.forEach((postDoc) => {
  console.log(postDoc.id, ' => ', JSON.stringify(postDoc.data()))
})
4
false
rHdnl1vDRjeXWD97paVc  =>  {"body":"body1","category":"life","likesCount":50,"title":"title1"}
u4g5Q58lbOROMy3TDvrF  =>  {"body":"body10","category":"life","likesCount":39,"title":"title10"}
vDJCwIo2vRpHf2jTkgaU  =>  {"body":"body4","category":"life","likesCount":28,"title":"title4"}
yu0IIVoaxzDtEEJ5vCBV  =>  {"body":"body8","category":"life","likesCount":47,"title":"title8"}

条件指定
 - 複合インデックスが必要なケース

Firestoreには、以下の2種類のインデックスがあります。

  • 単一フィールドインデックス(自動で設定)
  • 複合インデックス

以下のように複数フィールドに対してwhereを指定する場合、複合インデックスが必要です。

const querySnapshot = await db.collection('posts')
      .where('category', '==', 'life')
      .where('likesCount', '>', 40)
      .get()
723-firebase-firestore-query_read_index.png

並べ替え・件数指定
 - CollectionReference.orderBy()
 - CollectionReference.limit()

likesCount の降順で並べ替えて、5件取得してみます。

const querySnapshot = await db.collection('posts')
  .orderBy('likesCount', 'desc')
  .limit(5)
  .get()
console.log(querySnapshot.size)
console.log(querySnapshot.empty)
querySnapshot.forEach((postDoc) => {
  console.log(postDoc.id, ' => ', JSON.stringify(postDoc.data()))
})
5
false
rHdnl1vDRjeXWD97paVc  =>  {"body":"body1","category":"life","likesCount":50,"title":"title1"}
yu0IIVoaxzDtEEJ5vCBV  =>  {"body":"body8","category":"life","likesCount":47,"title":"title8"}
u0FMUCZJeUMljyt4QJwO  =>  {"body":"body12","category":"science","likesCount":43,"title":"title12"}
AOPFbwazI7HoE2ofXm5Q  =>  {"body":"body6","category":"science","likesCount":42,"title":"title6"}
OYCZTAtR7MNFnP1iMVX4  =>  {"body":"body3","category":"news","likesCount":40,"title":"title3"}

ページング
( 開始点, 終了点 )

startAt() startAfter() を利用してクエリの開始点を指定できます。
endAt() endBefore() を利用してクエリの終了点を指定できます。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const getPosts = async (cursor = null, type = '') => {
      let postRef = db.collection('posts')
        .orderBy('likesCount', 'desc')

      if (cursor) {
        console.log(`type: ${type}`)
        console.log(`cursor: ${cursor.data().title}`)

        if (type === 'startAt') {
          postRef = postRef.startAt(cursor)
        }

        if (type === 'startAfter') {
          postRef = postRef.startAfter(cursor)
        }

        if (type === 'endAt') {
          postRef = postRef.endAt(cursor)
        }

        if (type === 'endBefore') {
          postRef = postRef.endBefore(cursor)
        }
      }

      const querySnapshot = await postRef.get()
      console.log(`docs: ${querySnapshot.docs.map(postDoc => postDoc.data().title)}`)
      console.log()

      return querySnapshot.docs
    }

    const postDocs = await getPosts()
    await getPosts(postDocs[5], 'startAt')
    await getPosts(postDocs[5], 'startAfter')
    await getPosts(postDocs[5], 'endAt')
    await getPosts(postDocs[5], 'endBefore')

    await db.app.delete()
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()
docs: title1,title8,title12,title6,title3,title10,title5,title9,title7,title4,title11,title2

type: startAt
cursor: title10
docs: title10,title5,title9,title7,title4,title11,title2

type: startAfter
cursor: title10
docs: title5,title9,title7,title4,title11,title2

type: endAt
cursor: title10
docs: title1,title8,title12,title6,title3,title10

type: endBefore
cursor: title10
docs: title1,title8,title12,title6,title3

ページング処理例

以下のようにページング処理を実装できます。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const getPosts = async (cursor, limit) => {
      let postRef = db.collection('posts')
        .orderBy('likesCount', 'desc')
        .limit(limit)
      if (cursor) {
        postRef = postRef.startAfter(cursor)
      }
      const querySnapshot = await postRef.get()
      const lastVisibleDoc = !querySnapshot.empty
        ? querySnapshot.docs[querySnapshot.docs.length - 1]
        : null
      return {
        docs: querySnapshot.docs,
        cursor: lastVisibleDoc
      }
    }

    let cursor = null
    while (true) {
      console.log('########################################')
      const postDocs = await getPosts(cursor, 3)
      cursor = postDocs.cursor
      console.log(`docs: ${postDocs.docs.map(postDoc => postDoc.id)}`)
      console.log(`cursor: ${cursor && cursor.id}`)
      if (!cursor) {
        break
      }
    }

    await db.app.delete()
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()

3ドキュメントずつ読み込んでいます。

########################################
docs: rHdnl1vDRjeXWD97paVc,yu0IIVoaxzDtEEJ5vCBV,u0FMUCZJeUMljyt4QJwO
cursor: u0FMUCZJeUMljyt4QJwO
########################################
docs: AOPFbwazI7HoE2ofXm5Q,OYCZTAtR7MNFnP1iMVX4,u4g5Q58lbOROMy3TDvrF
cursor: u4g5Q58lbOROMy3TDvrF
########################################
docs: CAy51oSyQVpoayfY54k9,ThJZPUBdj95ozjfqgLJh,jaovhErVfrFudbkVEd8y
cursor: jaovhErVfrFudbkVEd8y
########################################
docs: vDJCwIo2vRpHf2jTkgaU,dEEUHM65w1B1CNinrZPF,VvuGkWmH9Hkd3LorLUaI
cursor: VvuGkWmH9Hkd3LorLUaI
########################################
docs: 
cursor: null

削除

ドキュメントを削除
 - CollectionReference.doc(docId)
 - DocumentReference.delete()

DocumentReferenceのdeleteメソッド で対象ドキュメントを削除できます。

await db.collection('users')
  .doc(documentPath)
  .delete()

ドキュメント内の一部データを削除
 - FieldValue.delete()

下記ドキュメントの score のみ削除してみます。

723-firebase-firestore-query_delete_field_01.png

一部データを削除する場合、FieldValue.delete を利用します。

const userRef = db.collection('users').doc('KRZ5TTHs4Xwjo7XK9t59')
await userRef.update({
  score: firebase.firestore.FieldValue.delete(),
  updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
})

以下、実行結果です。

723-firebase-firestore-query_delete_field_02.png

コレクションを削除

コレクション内のすべてのドキュメントが削除されれば削除されます。

複数ドキュメントに対してアトミック操作

トランザクション( Transaction )
( 読み込み後、書きこみ )

トランザクションを利用する場合、書き込み操作 より 読み取り操作 を先に実行する必要があります。

const collectionADocRef = db.collection('collectionB').doc('XXXXXXXXXXXXXXXXXXXX')
const collectionBDocRef = db.collection('collectionB').doc('YYYYYYYYYYYYYYYYYYYY')

await db.runTransaction(async transaction => {
  const [collectionADoc, collectionBDoc] = await Promise.all([
    transaction.get(collectionADocRef),
    transaction.get(collectionBDocRef),
  ])
  transaction.update(collectionADocRef, { title: `xxx_${collectionADoc.get('name')}` })
  transaction.update(collectionBDocRef, { name: `xxx_${collectionBDoc.get('name')}` })
})

バッチで一括書き込み( WriteBatch )
( 書き込みのみ )

Firestore.batch() を呼び出すと WriteBatchのオブジェクト を取得できます。

(async () => {
  try {
    // 省略 
    // (Cloud Firestoreのインスタンスを初期化してdbにセット)

    const batch = db.batch()
    batch.set(
      db.collection('posts').doc('7is0l08G9OtjedDt5o1h'),
      {title: 'xxx'}
    )

    batch.update(
      db.collection('posts').doc('et7WHlN49VaRmpINjwda'),
      {title: 'yyy'}
    )

    batch.delete(
      db.collection('posts').doc('JzsIRSiWVixyUuyZQN7m')
    )
    await batch.commit()

    await db.app.delete()
  } catch (err) {
    console.log(`Error: ${JSON.stringify(err)}`)
  }
})()

参考

わくわくBank.
技術系の記事を中心に、役に立つと思ったこと、整理したい情報などを掲載しています。