Firestore - 如何处理数组“不包含”查询

6
经过一番研究,似乎我不能使用FireStore来查询某个给定数组不包含的项。有没有人能提供针对这种情况的解决方法?
当用户注册后,应用程序会获取一堆卡片,每张卡片都对应着 FireStore 中的一个 "card" 文档。当用户与卡片交互后,卡片文档会将用户的用户 ID 添加到一个字段数组中(例如:usersWhoHaveSeenThisCard: [userUID]),而 "user" 文档会将卡片的用户 ID 添加到一个字段数组中(例如:cardsThisUserHasSeen: [cardUID])。"user" 文档位于 "user" 集合中,而 "card" 文档则位于 "card" 集合中。
目前,我想获取所有用户未与之交互的卡片。然而,这是有问题的,因为我只知道用户已经与哪些卡片交互过,所以 .whereField(usersWhoHaveSeenThisCard, arrayContains: currentUserUID) 就无法使用了,因为我需要一个"arrayDoesNotContain"语句,但它不存在。
最后,用户无法拥有一张卡片,因此我无法在卡片文档中创建真/假布尔字段(例如:userHasSeenThisCard: false)并按照这个标准进行搜索。
我能想到的唯一解决方案就是在卡片文档上创建一个新的字段数组,其中包含未看过卡片的每个用户(例如:usersWhoHaveNotSeenThisCard: [userUID]),但这意味着每个注册用户都必须将其用户 ID 写入 1000 多个卡片文档中,这将消耗我的数据。
我可能只是运气不好,但希望更熟悉 NOSQL / FireStore 的人能提供一些见解。
// If any code sample would help, please let me know and I'll update - I think this is largely conceptual as of now
2个回答

2

从查询限制中您已经发现,仅使用Cloud Firestore无法轻松解决此问题。您需要以某种方式存储已查看文档的列表,并在客户端应用程序中将其加载到内存中,然后从所有潜在文档的查询结果中手动减去这些文档。

您可能希望考虑增加另一个数据库来更清晰地执行此类操作(例如可以执行连接和子查询的SQL数据库),并同时维护它们。

或者,要求按可预测的顺序查看所有文档,例如按时间戳排序。然后,您只需存储上次查看文档的时间戳,并使用该时间戳过滤结果。


谢谢Doug,不幸的是我现在不能设置额外的数据库,但时间戳的想法可能会奏效。所以基本上按最旧的顺序排序并添加一个.whereField("timestamp", isGreaterThan: lastTimestampSeen) ,并将用户的lastTimestampSeen属性保存到用户文档中。虽然不完美,但现在应该足够了。 - bhersh90
@bhersh90 我认为我们可以在不添加或维护额外数据库甚至更改结构的情况下完成这个任务。我提供了一个答案-可能适用于您的用例。 - Jay

1

有一个被接受并且不错的答案,但它并没有直接解决问题,所以我来说一下...(这可能有用,也可能没用)

我不知道你的Firestore结构是什么,所以这里是我的假设:

cards
   card_id_0
      usersWhoHaveSeenThisCard
         0: uid_0
         1: uid_1
         2: uid_2
   card_id_1
      usersWhoHaveSeenThisCard
         0: uid_2
         1: uid_3
   card_id_2
      usersWhoHaveSeenThisCard
         0: uid_1
         1: uid_3

假设我们想知道 uid_2 还没有看过哪些卡片,这种情况下是 card_id_2。
func findCardsUserHasNotSeen(uidToCheck: String, completion: @escaping ( ([String]) -> Void ) ) {
    let ref = self.db.collection("cards")

    ref.getDocuments(completion: { snapshot, err in
        if let err = err {
            print(err.localizedDescription)
            return
        }

        guard let docs = snapshot?.documents else {
            print("no docs")
            return
        }
        var documentsIdsThatDoNotContainThisUser = [String]()
        for doc in docs {
            let uidArray = doc.get("usersWhoHaveSeenThisCard") as! [String]
            let x = uidArray.contains(uidToCheck)
            if x == false {
                documentsIdsThatDoNotContainThisUser.append(doc.documentID)
            }
        }
        completion(documentsIdsThatDoNotContainThisUser)
    })
}

然后,像这样使用用例。
func checkUserAction() {
    let uid = "uid_2" //the user id to check
    self.findCardsUserHasNotSeen(uidToCheck: uid, completion: { result in
        if result.count == 0 {
            print("user: \(uid) has seen all cards")
            return
        }
        for docId in result {
            print("user: \(uid) has not seen: \(docId)")
        }
    })
}

和输出

user: uid_2 has not seen: card_id_2

这段代码遍历文档,获取存储在每个文档的“usersWhoHaveSeenThisCard”节点中的uid数组,并确定uid是否在数组中。如果不在,则将该documentID添加到“documentsIdsThatDoNotContainThisUser”数组中。一旦检查完所有文档,就会返回不包含用户id的文档ID数组。
知道Firestore有多快后,我对大型数据集运行了此代码,并且结果非常快地返回,因此对于大多数用例,它不应引起任何类型的滞后。

谢谢Jay - 我真的很喜欢这种方法。我唯一的担忧是可能会创建大量不必要的读取。我不确定FireStore如何决定获取数据的顺序,但它似乎非常相似(即:cardUID₁,cardUID₂等)。如果是这种情况,那么用户将不得不通过他们之前看到的所有内容进行for-in循环,然后才能看到任何新内容,当他们浏览了数百张卡片时,这将很糟糕。让我知道你的想法 - 除此之外,我真的很喜欢这种方法。 - bhersh90
我应该澄清一下,我只想一次拉取10张卡片,并且当前在我的获取查询中有一个.limit(10)子句。 - bhersh90
@bhersh90 在这种情况下,读取操作是必要的,以确定用户尚未看到哪些卡片。如果数据集只有几千张卡片,那么影响应该很小。至于限制,应该很容易包括一个计数器,并在for循环达到10时退出。 - Jay
在docs for-in循环中,每个文档需要一次读取,对吗?如果平均用户已经查看了90张卡片,因此必须遍历100个文档才能获得10张新卡片的批次,则在进行500次这些获取请求后,我将达到我的50k读取限制。平均而言,用户每个会话会调用约5次获取操作,因此在计费开始之前,可以支持大约100个每日用户。打出来,实际上还不错。我想我可能会转换到这个解决方案。再次感谢! - bhersh90

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接