Firestore Paginating数据+ Snapshot监听器

时间:2017-11-08 14:54:31

标签: ios objective-c swift pagination google-cloud-firestore

我现在正在与Firestore合作,并且有一些分页问题 基本上,我有一个集合(假设10个项目),其中每个项目都有一些数据和时间戳。

现在,我正在获取前3个项目:

Firestore.firestore()
    .collection("collectionPath")
    .order(by: "timestamp", descending: true)
    .limit(to: 3)
    .addSnapshotListener(snapshotListener())

在我的快照侦听器中,我保存了快照中的最后一个文档,以便将其用作下一页的起点。

所以,在某些时候我会要求下一页这样的项目:

Firestore.firestore()
    .collection("collectionPath")
    .order(by: "timestamp", descending: true)
    .start(afterDocument: lastDocument)
    .limit(to: 3)
    .addSnapshotListener(snapshotListener2()) // Note that this is a new snapshot listener, I don't know how I could reuse the first one

现在我的前端有索引0到索引5(共6个)的项目。整齐!

如果索引4处的文档现在将其时间戳更新为整个集合的最新时间戳,则事情开始下降。
请记住,时间戳决定了它在订单子句中的位置!

我预计会发生的是,在应用更改后,我仍会显示6个项目(仍按时间戳排序)

发生了什么,在应用更改后,我只剩下5个项目,因为从第一个快照中推出的项目没有自动添加到第二个快照中。

我是否错过了与Firestore分页的内容?

编辑:根据要求,我在这里发布了更多代码:
这是我返回快照侦听器的函数。好吧,我使用两种方法来请求第一页,然后是我在上面发布的第二页

private func snapshotListener() -> FIRQuerySnapshotBlock {
    let index = self.index
    return { querySnapshot, error in
        guard let snap = querySnapshot, error == nil else {
            log.error(error)
            return
        }

        // Save the last doc, so we can later use pagination to retrieve further chats
        if snap.count == self.limit {
            self.lastDoc = snap.documents.last
        } else {
            self.lastDoc = nil
        }

        let offset = index * self.limit

        snap.documentChanges.forEach() { diff in
            switch diff.type {
            case .added:
                log.debug("added chat at index: \(diff.newIndex), offset: \(offset)")
                self.tVHandler.dataManager.insert(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)

            case .removed:
                log.debug("deleted chat at index: \(diff.oldIndex), offset: \(offset)")
                self.tVHandler.dataManager.remove(itemAt: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)

            case .modified:
                if diff.oldIndex == diff.newIndex {
                    log.debug("updated chat at index: \(diff.oldIndex), offset: \(offset)")
                    self.tVHandler.dataManager.update(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)
                } else {
                    log.debug("moved chat at index: \(diff.oldIndex), offset: \(offset) to index: \(diff.newIndex), offset: \(offset)")
                    self.tVHandler.dataManager.move(item: Chat(dictionary: diff.document.data() as NSDictionary), from: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), to: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)
                }
            }
        }
        self.tableView?.reloadData()
    }
}

所以再一次,我问我是否可以有一个快照侦听器来监听我从Firestore请求的多个页面中的更改

5 个答案:

答案 0 :(得分:3)

好吧,我联系了Firebase Google Group的人员寻求帮助,他们能告诉我我的用例还不支持。
感谢Kato Richardson参与解决我的问题!

对于对细节感兴趣的任何人,请参阅此thread

答案 1 :(得分:0)

我今天遇到了相同的用例,并且已经在Objective C客户端中成功实现了一个可行的解决方案。如果有人想在自己的程序中应用以下算法,那么如果google-cloud-firestore小组可以将我的解决方案放在他们的页面上,我将不胜感激。

用例:该功能允许对一长串近期聊天进行分页,并可以附加实时侦听器以更新列表,以便将最新消息放在最前面。

解决方案:这可以通过使用分页逻辑来实现,就像我们对其他长列表所做的那样,并将实时侦听器的限制设置为1:

第1步:在页面加载中,使用分页查询来获取聊天,如下所示:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
     [self fetchChats];
}

-(void)fetchChats {
    __weak typeof(self) weakSelf = self;
     FIRQuery *paginateChatsQuery = [[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:MAGConstPageLimit];
    if(self.arrChats.count > 0){
        FIRDocumentSnapshot *lastChatDocument = self.arrChats.lastObject;
        paginateChatsQuery = [paginateChatsQuery queryStartingAfterDocument:lastChatDocument];
    }
    [paginateChatsQuery getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
        if (snapshot == nil) {
            NSLog(@"Error fetching documents: %@", error);
            return;
        }
        ///2. Observe chat updates if not attached
        if(weakSelf.chatObserverState == ChatObserverStateNotAttached) {
            weakSelf.chatObserverState = ChatObserverStateAttaching;
            [weakSelf observeChats];
        }

        if(snapshot.documents.count < MAGConstPageLimit) {
            weakSelf.noMoreData = YES;
        }
        else {
            weakSelf.noMoreData = NO;
        }

        [weakSelf.arrChats addObjectsFromArray:snapshot.documents];
        [weakSelf.tblVuChatsList reloadData];
    }];
}

第2步:在“ fetchAlerts”方法的成功回调中,仅将观察者附加一次实时更新,且限制设置为1。

-(void)observeChats {
    __weak typeof(self) weakSelf = self;
    self.chatsListener = [[[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:1]addSnapshotListener:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
        if (snapshot == nil) {
            NSLog(@"Error fetching documents: %@", error);
            return;
        }
        if(weakSelf.chatObserverState == ChatObserverStateAttaching) {
            weakSelf.chatObserverState = ChatObserverStateAttached;
        }

        for (FIRDocumentChange *diff in snapshot.documentChanges) {
            if (diff.type == FIRDocumentChangeTypeAdded) {
                ///New chat added
                NSLog(@"Added chat: %@", diff.document.data);
                FIRDocumentSnapshot *chatDoc = diff.document;
                [weakSelf handleChatUpdates:chatDoc];

            }
            else if (diff.type == FIRDocumentChangeTypeModified) {
                NSLog(@"Modified chat: %@", diff.document.data);
                FIRDocumentSnapshot *chatDoc = diff.document;
                [weakSelf handleChatUpdates:chatDoc];
            }
            else if (diff.type == FIRDocumentChangeTypeRemoved) {
                NSLog(@"Removed chat: %@", diff.document.data);
            }
        }
    }];

}

第3步。在侦听器回调中,检查文档更改并仅处理 FIRDocumentChangeTypeAdded FIRDocumentChangeTypeModified 事件,并忽略 FIRDocumentChangeTypeRemoved 事件。为此,我们为 FIRDocumentChangeTypeAdded FIRDocumentChangeTypeModified 事件调用“ handleChatUpdates ”方法,在此方法中,我们首先尝试从中找到匹配的聊天文档本地列表,如果存在,则将其从列表中删除,然后添加从侦听器回调收到的新文档,并将其添加到列表的开头。

-(void)handleChatUpdates:(FIRDocumentSnapshot *)chatDoc {
    NSInteger chatIndex = [self getIndexOfMatchingChatDoc:chatDoc];
    if(chatIndex != NSNotFound) {
        ///Remove this object
        [self.arrChats removeObjectAtIndex:chatIndex];
    }
    ///Insert this chat object at the beginning of the array
     [self.arrChats insertObject:chatDoc atIndex:0];

    ///Refresh the tableview
    [self.tblVuChatsList reloadData];
}

-(NSInteger)getIndexOfMatchingChatDoc:(FIRDocumentSnapshot *)chatDoc {
    NSInteger chatIndex = 0;
    for (FIRDocumentSnapshot *chatDocument in self.arrChats) {
        if([chatDocument.documentID isEqualToString:chatDoc.documentID]) {
            return chatIndex;
        }
        chatIndex++;
    }
    return NSNotFound;
}

第4步。重新加载表格视图以查看更改。

答案 2 :(得分:0)

我的解决方案是创建1个维护者查询-侦听器以观察从第一个查询中删除的项目,并且每次有新消息出现时,我们都会对其进行更新。

答案 3 :(得分:0)

向快照侦听器添加分页是一个简单的过程。我实施一次它没有任何障碍。到目前为止,唯一的警告是它需要索引文档。索引文档是指保留查询结果清单(索引)的单个文档。这意味着,您不必侦听一组文档中的更新(如Firebase文档所概述的查询),您必须侦听单个文档的更新(无论如何,这样做更经济)。想想看:例如,如果您有一个聊天应用程序,例如用户有25个聊天连接,则每次在任何一个聊天中发送和接收消息时,您都必须向Firestore支付25次读取的费用(因为您正在收听整个查询)。如果用户进行100次聊天,向某人发送一个表情符号将使您花费100次阅读!两个用户之间的五分钟对话可能会花费您成千上万的读取次数。我相信,这就是为什么您在网上看到一些文章抱怨Firebase多么昂贵的原因。

所有这些的原因是因为Firestore快照侦听器仅侦听对其附加的查询结果的更改,包括分页限制!这意味着,如果您查询的结果为50个文档,而分页限制为10个文档,则快照侦听器将仅侦听该50个文档中的前10个文档中的更改。这是一个无用的侦听器。

因此,要使用OP的聊天应用程序,当一个用户向另一个用户发送聊天消息时,请将该操作包装在Firestore事务中,同时还要对索引文档进行更新。交易在Firestore中是原子性的,从而确保了集合(在查询中)中的文档将始终与索引文档同步。而所有此索引文档需要包含的是一个map,它使用userId作为键,而一个timestamp作为值。然后,当有人向用户发送消息时,索引文档将更新,通知侦听器(返回新索引),并且您需要做的就是在屏幕上加载与用户分页一样多的文档。用户已经浏览了30个结果,请从更新的索引中重新加载前30个文档。 Firestore与通过查询来获取文档一样,能够在for-in循环中获取文档。

请务必记住,Firestore为每个文档提供1MB的内存,即1,048,576字节。如果索引中的每个条目都是用户的ID(15-20个字节)和一个时间戳记(8个字节),则该索引可以记录25,000多个条目(或每个用户25,000个聊天连接)。 Firebase建议将小的,灵活的文档推荐给大型文档。但是,如果它们使我们可以使用一百万个字节,则有时您需要使用其中的几个字节,尤其是随着用户群的增长,它可以为您节省大量资金时。

答案 4 :(得分:0)

首先要通过快照监听器进行分页,我们必须从集合中创建reference point document。之后,我们将基于该reference point document来监听集合。​​

假设您有一个名为messages的集合,并且该集合中的每个文档都有一个名为createdAt的时间戳。

//get messages
getMessages(){

//first we will fetch the very last/latest document.

//to hold listeners
listnerArray=[];

const very_last_document= await this.afs.collectons('messages')
    .ref
    .limit(1)
    .orderBy('createdAt','desc')
    .get({ source: 'server' });

 
 //if very_last.document.empty property become true,which means there is no messages 
  //present till now ,we can go with a query without having a limit

 //else we have to apply the limit

 if (!very_last_document.empty) {

    
    const start = very_last_document.docs[very_last_document.docs.length - 1].data().createdAt;
    //listner for new messages
   //all new message will be registered on this listener
    const listner_1 = this.afs.collectons('messages')
    .ref
    .orderBy('createdAt','desc')
    .endAt(start)     <== this will make sure the query will fetch up to 'start' point(including 'start' point document)
    .onSnapshot(messages => {

        for (const message of messages .docChanges()) {
          if (message .type === "added")
            //do the job...
          if (message.type === "modified")
            //do the job...
          if (message.type === "removed")
           //do the job ....
        }
      },
        err => {
          //on error
        })

    //old message will be registered on this listener
    const listner_2 = this.afs.collectons('messages')
    .ref
    .orderBy('createdAt','desc')
    .limit(20)
    .startAfter(start)   <== this will make sure the query will fetch after the 'start' point
    .onSnapshot(messages => {

        for (const message of messages .docChanges()) {
          if (message .type === "added")
            //do the job...
          if (message.type === "modified")
            //do the job...
          if (message.type === "removed")
           //do the job ....
        }
       this.listenerArray.push(listner_1, listner_2);
      },
        err => {
          //on error
        })
  } else {
    //no document found!
   //very_last_document.empty = true
    const listner_1 = this.afs.collectons('messages')
    .ref
    .orderBy('createdAt','desc')
    .onSnapshot(messages => {

        for (const message of messages .docChanges()) {
          if (message .type === "added")
            //do the job...
          if (message.type === "modified")
            //do the job...
          if (message.type === "removed")
           //do the job ....
        }
      },
        err => {
          //on error
        })
    this.listenerArray.push(listner_1);
  }

}


//to load more messages
LoadMoreMessage(){

//Assuming messages array holding the the message we have fetched


 //getting the last element from the array messages.
 //that will be the starting point of our next batch
 const endAt = this.messages[this.messages.length-1].createdAt

  const listner_2 = this.getService
  .collections('messages')
  .ref
  .limit(20)
  .orderBy('createdAt', "asc")    <== should be in 'asc' order
  .endBefore(endAt)    <== Getting the 20 documnents (the limit we have applied) from the point 'endAt';
.onSnapshot(messages => {

if (messages.empty && this.messages.length)
  this.messages[this.messages.length - 1].hasMore = false;

for (const message of messages.docChanges()) {
  if (message.type === "added") 
  //do the job...

  if (message.type === "modified")
    //do the job

  if (message.type === "removed")
    //do the job
}

},
 err => {
    //on error
 })

 this.listenerArray.push(listner_2)



}