Android中的Firestore数据建模和查询定制
Firestore数据库是一个现代的、基于对象的数据库,以JSON格式存储数据。在本教程中,我们将学习如何准备数据、设计数据库,以及使用Android平台执行简单和复杂的查询。
前提条件
要继续学习本教程,你将需要以下条件。
- 在你的电脑上安装Android Studio。
- Kotlin和Coroutines的基本知识。
- 熟悉Firebase控制台。
创建数据模型
一个数据模型描述了存储在数据库中的数据。一个好的模型可以让你以一种易于使用和理解的方式来构造数据。这使得数据库更具有可维护性。
Firestore数据模型
在Firestore中,数据被存储在由唯一路径标识的集合中。每个集合都包含文档,它们是key-value 对。
一个文档可以包含任意数量的fields ,包括sub-documents 和sub-collections 。
在Android中实现Firestore
首先,在Android Studio中创建一个新项目。
然后前往Firebase控制台,创建一个新的Firebase项目。
这将要求你有一个谷歌账户。注意,你必须使用同一个账户来登录Android Studio和Firebase控制台。
接下来打开Firestore 标签,点击create database 按钮。这将创建一个新的Firestore数据库。
在你部署项目之前,使用test rules ,以确保数据库工作正常,这一点很重要。这可以通过点击test rules 按钮来完成。
警告。测试规则允许任何人访问数据库。因此,不建议在生产中使用。相反,你应该使用安全规则和认证来限制对数据库的访问。
最后在这一步,我们需要将我们的应用程序连接到Firebase。在Android Studio中点击Tools 标签,选择Firebase ,然后通过点击Connect to Firebase 按钮连接项目。
这将带你到Firebase控制台,你需要选择我们刚刚创建的项目。
Firebase assistant这样做之后,回到Android Studio,通过点击add cloud Firestore 中的按钮添加Firestore SDK 。
创建一个数据库实例
在你的活动文件中,添加一个FirebaseFirestore 实例,如下图所示。
lateinit var database: FirebaseFirestore
override fun onCreate(savedInstanceState: Bundle?) {
database = FirebaseFirestore.getInstance()
}
设计数据库
继续,我们需要用数据填充我们的数据库,我们将对其执行查询。
用例
让我们以一个农业农场为例,在这个农场里,我们有一系列的农作物,可以分为水果、蔬菜和树木。
要为此设计一个Firestore数据库,我们需要创建一个名为farm 的集合,该集合持有一个名为crops 的文档。这个文件将持有名为fruits 、vegetables 和trees 的子集合。
它看起来应该如下图所示。

你可以使用控制台或代码来设计,这将在本教程中讨论。
设计应用程序的用户界面
在你的布局文件中添加以下代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:padding="8dp"
android:layout_height="match_parent" >
<!--let's keep things simple 😎-->
<Button
android:id="@+id/btnSaveData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save data"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.85" />
<Button
android:id="@+id/btnReadData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Read data"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnSaveData"
app:layout_constraintVertical_bias="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
设计应用程序的数据模型
根据上面描述的用例,我们需要为crop 类别创建一个enum class ,并创建一个data class 来定义crops 对象/文件的结构。
枚举类
enum class CropCategory {
FRUIT, VEGETABLE, TREE
}
数据类
data class Crop(
val name: String,
var count: Int,
val category: CropCategory
)
生成虚拟数据
这里,我们将为我们的数据库创建随机数据。在一个生产项目中,你可能会从用户那里获得输入。
在你的活动文件中,创建一个函数,当点击Save data 按钮时将被触发。
private fun handleClicks() {
binding.btnSaveData.setOnClickListener {
// Create Sample data for demonstration purposes
val crops = arrayListOf<Crop>(
Crop("Apple", 10, CropCategory.FRUIT),
Crop("Apricots", 5, CropCategory.FRUIT),
Crop("Avocado", 8, CropCategory.FRUIT),
Crop("Banana", 24, CropCategory.FRUIT),
Crop("Blackberries", 16, CropCategory.FRUIT),
Crop("Broccoli", 3, CropCategory.VEGETABLE),
Crop("Brooklime", 20, CropCategory.VEGETABLE),
Crop("Brussels sprouts", 10, CropCategory.VEGETABLE),
Crop("Cabbage Brassica", 15, CropCategory.VEGETABLE),
Crop("White Ash", 5, CropCategory.TREE),
Crop("Bigot Aspen", 1, CropCategory.TREE),
Crop("Quaking Aspen", 3, CropCategory.TREE),
Crop("Basswood", 1, CropCategory.TREE),
Crop("American Beech", 3, CropCategory.TREE)
)
}
}
记住在你的MainActivity 的onCreate 方法中调用这个函数。
保存数据到Firestore
创建一个名为saveData 的函数并添加以下代码。
private fun saveData(crops: ArrayList<Crop>) {
// NOTE: This should be done in a ViewModel
val cropsDoc: DocumentReference = database.collection("farm")
.document("crops")
crops.forEach { crop ->
when (crop.category) {
CropCategory.FRUIT -> {
cropsDoc.collection("fruits").add(crop)
}
CropCategory.VEGETABLE -> {
cropsDoc.collection("vegetables").add(crop)
}
// this is a Tree
else -> {
cropsDoc.collection("trees").add(crop)
}
}
}
}
在这里,我们使用了crops 文档引用(cropsDoc)来保存每个作物在相应的子集合中。
当点击btnSaveData 按钮时,我们将调用saveData 函数并将crops 数组传递给一个coroutine,如下图所示。
// Launch a Coroutine to run the save-data Job
CoroutineScope(Dispatchers.IO).launch {
saveData(crops)
}
测试应用程序
在你的设备上打开应用程序后,点击btnSaveData 按钮,然后导航到Firebase console ,确认上传是否按预期进行。
注意:多次点击将产生重复的数据,尽管每个文件都有一个唯一的ID。这对测试来说是好的,但对生产来说不是。
你应该期望看到与此类似的东西。

执行查询
为了查询数据,我们将使用DocumentReference 类的get() 方法。这个方法返回一个DocumentSnapshot 对象。
Firestore查询运算符
以下是可用于数据查询的运算符。
- (<) 小于
- (>=) 大于或等于
- (==) 等于
- (>) 大于
- (<=) 小于或等于
- (array-contains) 检查值是否存在于数组中。
比较运算符在基于Android的数据库查询方法中被明确命名。
在where() 方法中,这些运算符与要过滤的字段和要比较的值一起作为参数之一使用。
你可能已经注意到firebase控制台的一个警告,表明crops 文件不会出现在查询中,因为它不存在。这是因为它只由集合组成。要解决这个问题,至少要向它添加一个文档。
简单查询
这些查询涉及一个单一的动作和条件。例如,要查询fruits 集合中的文档,我们可以使用以下代码。
val docReference = database.collection("farm/crops/fruits")
docReference.get()
如果你已经查询了相同的数据并缓存了结果,你可以切换数据源,如下图所示。
val docReference = database.collection("farm/crops/fruits")
docReference.get(Source.CACHE)
为了跟踪查询的状态或进度,我们可以使用addOnCompleteListener lambda函数,它允许你处理事件,如查询的成功或失败。
val docReference = database.collection("farm/crops/fruits")
docReference.get().addOnCompleteListener { queryTask ->
when {
queryTask.isSuccessful -> {
// handle success event
}
queryTask.isCanceled -> {
// handle cancellation event
}
else -> {
// handle any other event
}
}
}
为了获得一个特定的文档,我们可以使用get() 方法,并将文档的id作为参数传递。
这在删除或更新字段时非常有用,但你必须提供一个准确的id。
val documentId = "THEDOCID" // This can be gotten from a previous query
val doc = database.collection("farm/crop/fruits").document().get(documentId)
复合查询
在复合查询中,我们可以将多个条件结合起来,形成一个单一的查询。当我们想执行一个返回特定文档的查询时,这很有用。
我们还可以创建对象来执行复杂的查询。
例如,要查询fruits 集合中所有count 大于15的文档,我们可以使用以下代码。
val TAG = "MainActivity"
val docReference = database.collection("farm/crops/fruits")
docReference.whereGreaterThan("count", 15).get().addOnSuccessListener {querySnapshot ->
for (doc in querySnapshot) {
Log.d(TAG, "id: ${doc.id} name: ${doc.data.getValue("name")}")
}
}
上面的代码返回两个水果,Blackberries 和Banana ,因为它们的计数满足条件。参照假数据来确认。

多个运算符可以连锁起来,形成一个更具体的查询。例如,我们可以得到fruits 集合中的所有文件,这些文件的count 大于5 但小于20 。
val TAG = "MainActivity"
val docReference = database.collection("farm/crops/fruits")
// this serves as a range operator
docReference.whereGreaterThan("count", 5).whereLessThan("count", 20).get().addOnSuccessListener {querySnapshot ->
for (doc in querySnapshot) {
Log.d(TAG, "id: ${doc.id} name: ${doc.data.getValue("name")}")
}
}
注意,你不能过滤不属于同一个文档的字段。
自定义操作符的索引
索引是用来使查询更有效率的。Firestore在数据被保存时自动创建索引。然而,对于自定义查询,我们需要为所涉及的字段创建索引。
这可以通过导航到firebase控制台的indexes 标签,然后点击Add index 按钮来完成。至少要选择两个字段。
另一种创建索引的方法是运行应用程序,你会在LogCat的debug 部分得到一个链接,它引导你到控制台并为你填写字段。为了准确和简单起见,强烈建议这样做。
条件/过滤器操作的顺序很重要,因为第一个操作的结果会传递给链中的下一个操作。
因此,你应该总是从不太具体或最相关的条件开始。
总结
在本教程中,我们已经介绍了Firestore中数据建模的基本概念以及如何查询数据。
在本教程中获得的知识可以用来构建一个强大的、可扩展的应用程序。继续练习,以获得对自定义查询和索引的更好理解。