簡體   English   中英

如何獲取 Firebase Cloud Firestore 中存在/不存在特定字段的文檔?

[英]How do I get documents where a specific field exists/does not exists in Firebase Cloud Firestore?

在 Firebase Cloud Firestore 中,我在集合中有“user_goals”,目標可能是預定義的目標(master_id:“XXXX”)或自定義目標(沒有“master_id”鍵)

在 JavaScript 中,我需要編寫兩個函數,一個用於獲取所有預定義目標,另一個用於獲取所有自定義目標。

我有一些解決方法可以通過將“master_id”設置為“”空字符串來獲得自定義目標,並且能夠獲得如下:

db.collection('user_goals')
    .where('challenge_id', '==', '')  // workaround works
    .get()

仍然這不是正確的方法,我繼續將其用於具有“master_id”的預定義目標,如下所示

db.collection('user_goals')
    .where('challenge_id', '<', '')  // this workaround
    .where('challenge_id', '>', '')  // is not working
    .get()

由於 Firestore 沒有“!=”運算符,我需要使用“<”和“>”運算符但仍然沒有成功。

問題:忽略這些變通方法,通過檢查特定字段是否存在來獲取文檔的首選方法是什么?

作為@Emile Moureau 解決方案。 我更喜歡

.orderBy(`field`)

用字段來查詢文檔是否存在。 因為它可以處理任何類型的數據,即使是null也是如此。

但正如@Doug Stevenson 所說:

您無法查詢 Firestore 中不存在的內容。 字段需要存在才能讓 Firestore 索引知道它。

您無法查詢沒有該字段的文檔。 至少現在。

獲取存在指定字段的文檔的首選方法是使用:

.orderBy(fieldPath)

Firebase 文檔中所述:

在此處輸入圖像描述

因此@hisoft 提供的答案是有效的。 我只是決定提供官方來源,因為問題是針對首選方式。

我使用的解決方案是:

使用: .where('field', '>', ''),

其中“字段”是我們正在尋找的字段!

Firestore 是一個索引數據庫 對於文檔中的每個字段,該文檔會根據您的配置適當地插入到該字段的索引中 如果文檔不包含特定字段(如challenge_id ),它將不會出現在該字段的索引中,並且將從對該字段的查詢中省略。 通常,由於 Firestore 的設計方式,查詢應該在一次連續掃描中讀取索引。 在引入!=not-in運算符之前,這意味着您不能排除特定值,因為這需要跳過索引的各個部分。 嘗試在單個查詢中使用獨占范圍 ( v<2 || v>4 ) 時,仍然會遇到此限制。

字段值根據實時數據庫排序順序進行排序,但在遇到重復時,結果可以按多個字段排序,而不僅僅是文檔的 ID。

Firestore 值排序順序

優先 排序值 優先 排序值
1 null 6 字符串
2 false 7 文件參考
3 true 8 地理點
4 數字 9 數組
5 時間戳 10 地圖

不等式!= / <>

本節記錄了在2020 年 9 月發布!=not-in運算符之前不等式是如何工作的。 請參閱有關如何使用這些運算符的文檔 以下部分將留作歷史用途。

要在 Firestore 上執行不等式查詢,您必須重新編寫查詢,以便可以通過讀取 Firestore 的索引來讀取它。 對於不等式,這是通過使用兩個查詢來完成的——一個用於小於equality的值,另一個用於大於等於的值。

作為一個簡單的例子,假設我想要不等於 3 的數字。

const someNumbersThatAreNotThree = someNumbers.filter(n => n !== 3)

可以寫成

const someNumbersThatAreNotThree = [
   ...someNumbers.filter(n => n < 3),
   ...someNumbers.filter(n => n > 3)
];

將此應用於 Firestore,您可以轉換此( 以前)不正確的查詢:

const docsWithChallengeID = await colRef
  .where('challenge_id', '!=', '')
  .get()
  .then(querySnapshot => querySnapshot.docs);

進入這兩個查詢並合並它們的結果:

const docsWithChallengeID = await Promise.all([
  colRef
    .orderBy('challenge_id')
    .endBefore('')
    .get()
    .then(querySnapshot => querySnapshot.docs),
  colRef
    .orderBy('challenge_id')
    .startAfter('')
    .get()
    .then(querySnapshot => querySnapshot.docs),
]).then(results => results.flat());

重要提示:請求用戶必須能夠讀取與查詢匹配的所有文檔,以免出現權限錯誤。

缺失/未定義的字段

簡而言之,在 Firestore 中,如果某個字段未出現在文檔中,則該文檔將不會出現在該字段的索引中。 這與實時數據庫形成對比,其中省略字段的值為null

由於 NoSQL 數據庫的性質,您正在使用的架構可能會發生變化,從而使您的舊文檔缺少字段,您可能需要一個解決方案來“修補您的數據庫”。 為此,您將遍歷您的集合並將新字段添加到缺少它的文檔中。

為避免權限錯誤,最好使用帶有服務帳戶的 Admin SDK 進行這些調整,但您可以使用對數據庫具有適當讀/寫訪問權限的用戶使用常規 SDK 來執行此操作。

此函數是遞歸的,旨在執行一次

async function addDefaultValueForField(queryRef, fieldName, defaultFieldValue, pageSize = 100) {
  let checkedCount = 0, pageCount = 1;
  const initFieldPromises = [], newData = { [fieldName]: defaultFieldValue };

  // get first page of results
  console.log(`Fetching page ${pageCount}...`);
  let querySnapshot = await queryRef
    .limit(pageSize)
    .get();

  // while page has data, parse documents
  while (!querySnapshot.empty) {
    // for fetching the next page
    let lastSnapshot = undefined;

    // for each document in this page, add the field as needed
    querySnapshot.forEach(doc => {
      if (doc.get(fieldName) === undefined) {
        const addFieldPromise = doc.ref.update(newData)
          .then(
            () => ({ success: true, ref: doc.ref }),
            (error) => ({ success: false, ref: doc.ref, error }) // trap errors for later analysis
          );

        initFieldPromises.push(addFieldPromise);
      }

      lastSnapshot = doc;
    });

    checkedCount += querySnapshot.size;
    pageCount++;

    // fetch next page of results
    console.log(`Fetching page ${pageCount}... (${checkedCount} documents checked so far, ${initFieldPromises.length} need initialization)`);
    querySnapshot = await queryRef
      .limit(pageSize)
      .startAfter(lastSnapshot)
      .get();
  }

  console.log(`Finished searching documents. Waiting for writes to complete...`);

  // wait for all writes to resolve
  const initFieldResults = await Promise.all(initFieldPromises);

  console.log(`Finished`);

  // count & sort results
  let initializedCount = 0, errored = [];
  initFieldResults.forEach((res) => {
    if (res.success) {
      initializedCount++;
    } else {
      errored.push(res);
    }
  });

  const results = {
    attemptedCount: initFieldResults.length,
    checkedCount,
    errored,
    erroredCount: errored.length,
    initializedCount
  };

  console.log([
    `From ${results.checkedCount} documents, ${results.attemptedCount} needed the "${fieldName}" field added.`,
    results.attemptedCount == 0
      ? ""
      : ` ${results.initializedCount} were successfully updated and ${results.erroredCount} failed.`
  ].join(""));

  const errorCountByCode = errored.reduce((counters, result) => {
    const code = result.error.code || "unknown";
    counters[code] = (counters[code] || 0) + 1;
    return counters;
  }, {});
  console.log("Errors by reported code:", errorCountByCode);

  return results;
}

然后,您將使用以下方法應用更改:

const goalsQuery = firebase.firestore()
  .collection("user_goals");

addDefaultValueForField(goalsQuery, "challenge_id", "")
  .catch((err) => console.error("failed to patch collection with new default value", err));

也可以調整上述函數以允許根據文檔的其他字段計算默認值:

let getUpdateData;
if (typeof defaultFieldValue === "function") {
  getUpdateData = (doc) => ({ [fieldName]: defaultFieldValue(doc) });
} else {
  const updateData = { [fieldName]: defaultFieldValue };
  getUpdateData = () => updateData;
}

/* ... later ... */
const addFieldPromise = doc.ref.update(getUpdateData(doc))

正如您正確指出的那樣,無法根據!=進行過濾。 如果可能,我會添加一個額外的字段來定義目標類型。 可以在安全規則中使用!=以及各種字符串比較方法,因此您可以根據您的challenge_id格式強制執行正確的目標類型。

指定目標類型

創建一個type字段並基於此字段進行過濾。

type: mastertype: custom並搜索.where('type', '==', 'master')或搜索 custom。

標記自定義目標

創建一個可以為truefalsecustomGoal字段。

customGoal: true並搜索.where('customGoal', '==', true)或 false(根據需要)。

更新

現在可以在 Cloud Firestore 中執行 != 查詢

Firestore 確實接受了布爾值,這是一回事! 並且可以orderBy 'd。

所以經常,就像現在一樣,為此,我將它添加到來自onSnapshotget的數組推送中,使用.get().then(用於開發...

if (this.props.auth !== undefined) {
  if (community && community.place_name) {
    const sc =
      community.place_name && community.place_name.split(",")[1];
      const splitComma = sc ? sc : false
    if (community.splitComma !== splitComma) {
      firebase
        .firestore()
        .collection("communities")
        .doc(community.id)
        .update({ splitComma });
    }
    const sc2 =
      community.place_name && community.place_name.split(",")[2];
      const splitComma2 =sc2 ? sc2 : false
    console.log(splitComma2);
    if (community.splitComma2 !== splitComma2) {
      firebase
        .firestore()
        .collection("communities")
        .doc(community.id)
        .update({
          splitComma2
        });
    }
  }

這樣,我可以使用orderBy而不是where

browseCommunities = (paginate, cities) => {
  const collection = firebase.firestore().collection("communities");
    const query =
      cities === 1 //countries
        ? collection.where("splitComma2", "==", false) //without a second comma
        : cities //cities
        ? collection
            .where("splitComma2", ">", "")
            .orderBy("splitComma2", "desc") //has at least two
        : collection.orderBy("members", "desc");
  var shot = null;
  if (!paginate) {
    shot = query.limit(10);
  } else if (paginate === "undo") {
    shot = query.startAfter(this.state.undoCommunity).limit(10);
  } else if (paginate === "last") {
    shot = query.endBefore(this.state.lastCommunity).limitToLast(10);
  }
  shot &&
    shot.onSnapshot(
      (querySnapshot) => {
        let p = 0;
        let browsedCommunities = [];
        if (querySnapshot.empty) {
          this.setState({
            [nuller]: null
          });
        }
        querySnapshot.docs.forEach((doc) => {
          p++;
          if (doc.exists) {
            var community = doc.data();
            community.id = doc.id;

這不是一個理想的解決方案,但是當字段不存在時,這是我的解決方法:

let user_goals = await db.collection('user_goals').get()
user_goals.forEach(goal => {
  let data = goal.data()
  if(!Object.keys(data).includes(challenge_id)){
    //Perform your task here
  }
})

請注意,它會極大地影響您的讀取計數,因此只有在您的收藏量較小或負擔得起讀取時才使用它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM