Firestore 分页数据 + 快照监听器

Posted

技术标签:

【中文标题】Firestore 分页数据 + 快照监听器【英文标题】:Firestore Paginating data + Snapshot listener 【发布时间】:2018-04-21 07:43:06 【问题描述】:

我现在正在使用 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 处的文档现在将其时间戳更新为整个集合的最新时间戳,则情况开始下降。 请记住,时间戳根据 order 子句确定其位置!

我预期会发生的是,在应用更改后,我仍然显示 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 请求的多个页面中的更改

【问题讨论】:

分享您的代码以及您如何要求分页以便其他人可以帮助您 @AliAdil 我添加了更多代码(实际上是我现在使用的所有代码) 这里是我的工作解决方案的链接:***.com/a/53914090/3412051 我知道这是一个旧线程,但是,我现在写了一些关于这个的东西。也许它可以帮助你 - rizwaniqbal.com/posts/… @skaldesh 你找到解决方案了吗? 【参考方案1】:

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

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

【讨论】:

您最终找到解决方案了吗? 不,我们决定接受折衷方案,只更改首页加载。我们可以做到这一点,因为我们实现了一个简单的聊天,新消息只在开始时出现。但这远非完美的解决方案:/ 好的,谢谢 :) 我发现这可能会有所帮助:medium.com/@wcandillon/firebase-live-pagination-474748853e52 有趣!但我认为他描述的方式正是我尝试过的。他不会尝试在页面中插入一个项目,从而导致该页面的项目被推入另一个页面......我的问题是什么。不过,感谢您的链接! 我已经在 Objective-C 中实现了一个可行的解决方案,它在我的应用程序中运行,这里是它的链接:***.com/a/53914090/3412051【参考方案2】:

我今天遇到了同样的用例,我已经成功地在 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. 在侦听器回调中检查文档更改并仅处理 FIRDocumentChangeTypeAddedFIRDocumentChangeTypeModified 事件并忽略 FIRDocumentChangeTypeRemoved 事件。我们通过为 FIRDocumentChangeTypeAddedFIRDocumentChangeTypeModified 事件调用“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 步。重新加载 tableview 以查看更改。

【讨论】:

你可能已经注意到 Swift 标签了!你能把 Objective-C 转换成 Swift 吗? 由于还没有人提出将数据与监听器一起分页的解决方案,所以我用我熟悉的语言给出了解决方案。我认为现在解决方案比语言更重要,您可以通过阅读我用简单英语给出的解释以及通过代码轻松将其转换为 swift。 如果应用程序需要对现有文档进行实时更新,则此解决方案不起作用,因为您只收听最新文档,其余文档通过 getDocuments 获取。 我考虑了实时更新最近聊天列表的用例,其中我观察到最后一条消息时间戳,只要有新的更新(或消息),它就会始终更新。您也可以通过在文档更新时更新时间戳(例如名为 updatedAt 的字段)并改为观察该字段来在您的用例中进行这项工作。 @willbattel 我刚刚回复了您的 Google 帖子,并对此线程进行了详细解释:groups.google.com/forum/#!topic/firebase-talk/L-maF89Hc9M 希望对您有所帮助。【参考方案3】:

我的解决方案是创建 1 个维护者查询 - 侦听器来观察从第一个查询中删除的项目,每次有新消息到来时我们都会更新它。

【讨论】:

【参考方案4】:

要使用快照监听器首先进行分页,我们必须从集合中创建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)




【讨论】:

以上是关于Firestore 分页数据 + 快照监听器的主要内容,如果未能解决你的问题,请参考以下文章

是否应该避免同时添加多个 Firestore 快照侦听器?

Firestore:如果两个侦听器监听相同的查询 firebase 维护两个不同的查询快照?

如何从 Vuex 商店中删除 Firestore 快照侦听器

区分 Firestore 中的第一次查询快照和更改侦听器

在 React Native 中添加新消息时,Firestore 侦听器从分页中删除消息

我应该啥时候分离我的 Firestore 监听器? [复制]