구글 클라우드 Firestore의 문서를 어떻게 계산합니까?

문제 설명


모든 알려진 관계 데이터베이스와 많은 NoSQL 데이터베이스에는 문서/행/테이블의 총 수를 가져오는 데 사용할 수 있는 매우 간단한 인터페이스가 있습니다.이 인터페이스는 일반적으로 DB 엔진이 한 상자에서 지원합니다.
Firestore를 처음 사용하기 시작한 대부분의 개발자들은 클라우드 Firestore SDK에서도 Firestore를 사용할 수 있기를 희망한다.그러나 그것은 내장된 기능이 없다.
Firestoregithub 공식 저장소는 몇 년 전에 만든'count ()documents'함수에 대한 기능 요청이 있습니다.논평을 보면 한 팀이 미래 버전에서 하나의 기능을 실현할 계획이 없다는 것을 이해할 수 있다.
https://github.com/firebase/firebase-js-sdk/issues/236
Firestore 집합의 문서 총수를 계산하는 방법을 찾으려면, 대량의 StackOverflow 문제를 발견할 수 있습니다.그것들은 모두 각양각색의 해독과 변통 방법이 있고 많은 한계성과 결함이 있다.

가능한 솔루션


나는 내가 찾은 모든 가능한 해결 방안을 자세히 연구하고 그것들의 결점을 분석하려고 한다.

스냅샷 크기


첫 번째 해결 방안은 매우 간단하고 직접적이다.그 목적은 모든 파일을 얻고 수량을 점검하는 것이다.
db.collection('collectionName').get()
  .then(snapshot => console.log(snapshot.size));
✅ 실현하기 쉽다.
✅ 좋습니다. 소규모 컬렉션(10~250개의 문서)에 적용됩니다.
❌ 대량 집합 (1000여 개의 문서) 에 대해 잘못된 값을 되돌려줍니다.
❌ 간단한 작업의 지연을 증가시킵니다. (숫자를 계산하기 전에 모든 문서를 추출해야 합니다.)
❌ Firestore 읽기 제한 초과 사용 - 각 작업에는 ~ 1이 아닌 N 개의 읽기 작업이 사용됩니다.분명히 이것은 너의 예산을 신속하게 증가시킬 것이다.

작성 시 쓰기


첫 번째 생각은 계수를 단독 집합에 저장하는 것일 수도 있다.새 프로젝트를 만들 때마다 추가합니다.
const newDocData = { /* */ };

const docCollection = admin.firestore().collection('collectionName');
const statisticCollection = admin.firestore().collection('statCollectionName');
// create new document in collection
return docCollection.doc().set(newDocData).then(() => {
  // increase collection counter value
  return statisticCollection.update({
    docCounter: admin.firestore.FieldValue.increment(+1)
  });
});
✅ 모든 파일을 인벤토리할 필요가 없습니다.Firestore 예산 절감
❌ 카운터를 변경하려면 문서를 만들거나 삭제하는 각 위치에 코드를 배치해야 합니다.트랜잭션을 처리하기 어렵거나 대량 생성/삭제 중인 오류입니다.
❌ Firestore Web UI/Firestore 클라이언트에서 생성/제거한 항목을 처리할 수 없습니다.

쓰기 탐지기


구글 구름 함수/Firebase 함수 - 특수 이벤트에서 촉발할 수 있는 Lambda 함수를 만들 수 있습니다.
Firestore에는 추적 컬렉션/문서 쓰기 작업이 있는 이벤트가 있습니다.이런 문제에 대해 it의 실현은 보기에 원생적이고 유기적이다.
인터넷에는 이런 해결 방안에 관한 참고 자료가 매우 많다.
const statisticCollection = admin.firestore().collection('statCollectionName');
// setup cloud function listener
export const documentWriteListener = functions.firestore
  .document('collectionName/{id}')
  .onWrite((change, context) => {

    if (!change.before.exists) { // if new document created
      statisticCollection.update({
        docCounter: admin.firestore.FieldValue.increment(+1)
      });
    } else if (change.before.exists && change.after.exists) {
      // document updated - Do nothing
    } else if (!change.after.exists) { // document deleted
      statisticCollection.update({
        docCounter: admin.firestore.FieldValue.increment(-1)
      });
    }

  return;
});
❌ 이것은 보기에는 완벽한 해결 방안으로 보이지만, 정상적으로 일을 할 수 없다.이 함수를 실행한 다음 문서 (예: 100) 를 만듭니다.최종 카운터 값은 100보다 큽니다.
이 해결 방안의 오류와 왜 그것이 예상대로 작동하지 않았는지 조사해 봅시다.

Firestore 트리거 제한 사항



마지막으로 as에게 모든 트리거 함수는 적어도 한 번 실행될 것이라고 알려 줍니다.이것은 일부 문제, 실례 복제 등이 발생한 상황에서 몇 번 촉발될 수 있다는 것을 의미한다.
이것은 완벽한 해결 방안을 만들기 위해 우리가 명심해야 할 요점이다.

최종 솔루션


최종 솔루션은 쓰기 탐지기 솔루션을 기반으로 합니다.그러나 우리는 계수기의 중복 쓰기를 복구해야 한다.다중 계수기의 해결 방안을 개선했다.
각 Firestore 이벤트에는 컨텍스트 ID가 있습니다. 생성/삭제 작업마다 고유한 ID가 보장됩니다.
먼저 ID별로 이벤트를 저장하기 위한 별도의 집합을 만듭니다. 각 이벤트는 몇 개의 필드, 타임 스탬프, 집합 이름과 값을 포함하는 별도의 문서가 되어야 합니다.
// a list of collections names
const collectionsToSave = [
    COLLECTIONS.USER,
    COLLECTIONS.POST,
    COLLECTIONS.TAG,
    COLLECTIONS.COMMENTS,
];

const docEventsTrigger = () => {
  // trigger on all collections and documents
  return functions.firestore.document('{collectionId}/{docId}')
    .onWrite((change, context) => {
      // cut out all events that not related to our collections
      if (!collectionsToSave.includes(context.params.collectionId))
        return Promise.resolve();
      // cut out all update events
      if (change.before.exists && change.after.exists)
        return Promise.resolve();
      // store event and collection id
      const id = context.eventId;
      const collection = context.params.collectionId;
      // create a server timestamp value
      const timestamp = admin.firestore.FieldValue.serverTimestamp();
      // set a value +1 if new document created, -1 if document was deleted
      const value = !change.before.exists ? 1 : -1;
      // create new Event
      const newEventRef = admin.firestore().collection(COLLECTIONS.ADMIN_EVENTS).doc(id);
      // set data to new event and save
      return newEventRef.set({ collection, timestamp, value });
  });
};
이벤트가 정상적인지 확인하기 위해 이 트리거를 실행하고 항목을 만듭니다.

다음 단계에서는 이 사건들을 계수하고 숫자를 단독 집합에 쓸 것입니다.이벤트 수집 정리의 개선으로우리는 더 이상 이런 가치가 필요하지 않기 때문이다.(저부하 시스템의 경우 하루에 100개 미만의 이벤트는 건너뛸 수 있습니다.)
// a separate function to count events values
const calcCollectionIncrease = (docs, collectionName) => { 
  return docs
    // get only events of current collection
    .filter(d => d.collection === collectionName)
    // calc total sum of event values
    .reduce((res, d) => (res + d.value), 0);
};

const collectionsToCheck = [
    COLLECTIONS.USER,
    COLLECTIONS.POST,
    COLLECTIONS.TAG,
    COLLECTIONS.COMMENTS,
];

const docEventsCleanUp = () => {
  // scheduled run on every 5 minutes, can be extended due to your system load.
  return functions.pubsub.schedule('every 5 minutes')
    .onRun((context) => {
      // we will take only old event, that was cr3eated more than 5 minutes ago
      const limitDate = new Date(new Date() - (1000*60*5));
      // get 250 last events, sorted from old to new
      const lastEvents = admin.firestore()
        .collection(COLLECTIONS.ADMIN_EVENTS)
        .where('timestamp', '<', limitDate)
        .orderBy('timestamp', 'asc').limit(250);
      // ref to statistic document 
      const statDocRef = admin.firestore().doc(COLLECTIONS.ADMIN_STAT_DATA_COUNT);

      return admin.firestore()
        .runTransaction(t => (t.get(lastEvents).then(snap => {
          // if no events do nothing
          if (snap.empty) return Promise.resolve(0);

          const size = snap.size;
          // map data for all docs to reuse it later
          const snapData = snap.docs.map(d => d.data());
          // Dictionary to store counters
          const updateCountersDict = {};
          // Count events values per collection
          collectionsToCheck.forEach(collection => {
            updateCountersDict[collection] = admin.firestore.FieldValue
              .increment(calcCollectionIncrease(snapData, collection));
          });
          // updat4e counters
          t.update(statDocRef, updateCountersDict);
            // in case counters was successfully updated, delete old events
            snap.docs.map(d => t.delete(d.ref));
            return size;
          })))
        // log result to google cloud log for debug
        .then(result => console.log('Transaction success', result))
        .catch(err => console.log('Transaction failure:', err));
  });
};
한 줄에서 limitDate를 값(currentTime-5min)으로 설정합니다.제한 파일에 한 가지가 있기 때문이다.트리거당 최대 10초가 걸린다는 뜻이다.
마지막으로, 우리는 문서 번호의 정확한 값을 얻어야 한다.이벤트에서 카운터로 옮겨지지 않아도
이 스크립트는 현재 집합에 마지막으로 저장된 계수기 + 계수 이벤트를 가져오기 위해 간단한 스크립트를 사용할 수 있습니다.
const collectionToCheck = COLLECTIONS.TAG;
// ref to statistic document
const keyStatCountRef = admin.firestore().doc(COLLECTIONS.ADMIN_STAT_DATA_COUNT).get();
// ref to events collection filtered by one tag
const keyEvents = admin.firestore().collection(COLLECTIONS.ADMIN_EVENTS)
    .where('collection', '==', collectionToCheck).get();
// simultaneously run to query
Promise
  .all([keyStatCount, keyEvents])
  .then(([doc, eventsSnap]) => {
    // last counter value
    const statCount = doc.data()[collectionToCheck];
    // events value
    const eventsSum = eventsSnap.docs.map(d => d.data().value).reduce((res, val) => res + val, 0);

    return statCount + eventsSum;
  });
✅ 정상적으로 일하다
❌ 쓰기 작업을 2N+삭제 작업 1N으로 두 배 증가시킵니다.그러나 하나의 카운터 읽기 작업은 1이 필요합니다. (첫 번째 해결 방안은 읽을 때마다 N이 필요합니다.)
❌ 복잡한 설정.가장 좋은 것은 더욱 간단한 해결 방안이 있는 것이다. 그러나.

나는 현재의 해결 방안을 테스트하여 몇 초 안에 2k개가 넘는 문서를 대량으로 만들고 삭제했다.오랫동안 모든 것이 정상이었다.
👌 읽어주셔서 감사합니다.나는 나의 글이 같은 문제에 직면한 사람들에게 도움이 되기를 바란다.
🙌 우리가 해결 방안을 토론하고 개선할 수 있도록 당신의 경험을 공유합니다.

🏗️언제든지 내 측면 항목을 확인하십시오.


dummyapi.io
rgbtohex.page

좋은 웹페이지 즐겨찾기