Firestore未投票Poll筛选与数据模型设计技术咨询
针对你用Firestore做投票系统遇到的两个问题,我给你整理了实战性的解决方案,都是基于Firestore的最佳实践来的:
1. 数据模型设计方案
根据用户只能参与未投过的投票这个核心规则,推荐两种数据模型设计,适配不同的业务规模:
基础版:User文档存储已投票ID(适合中小规模场景)
这种方案简单直接,维护成本低:
- Poll集合(
polls):每个文档对应一个投票,包含筛选条件和基本信息:{ "id": "poll_123", "title": "你最喜欢的编程语言?", "options": ["Java", "Kotlin", "Swift"], "ageRange": [18, 40], "gender": { "male": true, "female": true, "other": true }, "createdAt": 1716201600000, // 用时间戳类型方便排序 "voteCount": 0 // 可选,用于快速统计总票数 } - User集合(
users):每个用户文档记录基本信息,以及已参与的投票ID数组:{ "id": "user_456", "dateOfBirth": "1998-05-15", "gender": "male", "votedPolls": ["poll_123", "poll_789"] // 存储已投过的Poll ID }
进阶版:独立Vote集合(适合高并发/大量投票场景)
如果用户投票次数极多(比如上千次),User文档的votedPolls数组可能接近Firestore单文档1MB的限制,这时可以拆分出独立的投票记录集合:
- Vote集合(
votes):每个文档记录一次投票行为:
这种方式可以避免单个User文档过大,查询用户已投票列表时,只需查询{ "userId": "user_456", "pollId": "poll_123", "votedAt": 1716205200000, "selectedOption": "Kotlin" }votes集合中该用户的所有记录,提取pollId即可。
2. 解决“筛选用户未投票Poll”的问题
Firestore确实不支持!=或者whereNotArrayContains这类反向查询,结合你的现有查询逻辑,推荐以下方案:
方案一:客户端过滤(最常用,简单高效)
先执行你现有的条件查询(年龄+性别筛选),获取符合要求的Poll列表,然后在客户端过滤掉用户已投过的Poll:
// 先拿到用户已投票的ID列表 val userVotedPolls = user?.votedPolls ?: emptyList() // 执行原有的Firestore查询 db.collection(POLLS) .whereArrayContains(Poll.AGE_RANGE, DateTimeUtils.calculateAge(user?.dateOfBirth)) .whereEqualTo("gender.${user?.gender}", true) .orderBy(Poll.CREATED_AT) .limit(QUEUE_LIMIT) .get() .addOnSuccessListener { querySnapshot -> // 客户端过滤掉已投票的Poll val availablePolls = querySnapshot.documents.mapNotNull { it.toObject(Poll::class.java) } .filter { poll -> !userVotedPolls.contains(poll.id) } // 这里处理可用的投票列表 updateVoteUI(availablePolls) }
这种方案在Poll数量不是特别大的时候完全够用,而且开发成本低。
方案二:分页补充查询(适合大规模Poll场景)
如果一次查询返回的Poll里大部分都是用户已投过的,导致可用数量不足,可以结合分页继续查询:
private fun fetchAvailablePolls(lastDocument: DocumentSnapshot? = null) { val userVotedPolls = user?.votedPolls ?: emptyList() var query = db.collection(POLLS) .whereArrayContains(Poll.AGE_RANGE, DateTimeUtils.calculateAge(user?.dateOfBirth)) .whereEqualTo("gender.${user?.gender}", true) .orderBy(Poll.CREATED_AT) .limit(QUEUE_LIMIT) if (lastDocument != null) { query = query.startAfter(lastDocument) } query.get().addOnSuccessListener { snapshot -> val filtered = snapshot.documents.mapNotNull { it.toObject(Poll::class.java) } .filter { !userVotedPolls.contains(it.id) } if (filtered.size < QUEUE_LIMIT && !snapshot.isEmpty) { // 如果可用数量不够,继续分页查询 fetchAvailablePolls(snapshot.last()) } else { // 处理最终的可用列表 updateVoteUI(filtered) } } }
避坑提示:不要在Poll文档中存已投票用户列表
很多人会想在Poll文档里加votedUsers数组,但如果一个投票有几千甚至上万人参与,这个数组会迅速超过Firestore单文档的大小限制,导致后续写入失败,绝对要避免这种设计。
内容的提问来源于stack exchange,提问作者Zohaib Akram




