在 Spring Boot 中使用 Google Cloud Firestore 資料庫
1. 概述
如今,雲端託管的託管資料庫變得越來越流行。其中一個範例是Cloud Firestore ,它是Firebase 和 Google提供的 NoSQL 文件資料庫,它為行動和 Web 應用程式提供按需可擴展性、靈活的資料建模以及離線支援。
在本教學中,我們將探討如何在 Spring Boot 應用程式中使用 Cloud Firestore 進行資料持久化。為了使我們的學習更加實用,我們將創建一個基本的任務管理應用程序,該應用程式允許我們使用 Cloud Firestore 作為後端資料庫來建立、檢索、更新和刪除任務。
2.雲端Firestore 101
在深入實施之前,我們先來了解 Cloud Firestore 的一些關鍵概念。
在 Cloud Firestore 中,資料儲存在文件中,並分組為集合。集合是文件的容器,每個文件包含一組不同資料結構的鍵值對,例如 JSON 物件。
Cloud Firestore 對文件路徑使用分層命名約定。文件路徑由集合名稱和後跟文件 ID 組成,並以正斜線分隔。例如, tasks/001
表示tasks
集合中 ID 為001
的文件。
3. 設定項目
在開始與 Cloud Firestore 互動之前,我們需要包含 SDK 依賴項並正確配置我們的應用程式。
3.1.依賴關係
首先,我們將Firebase 管理相依性新增至專案的pom.xml
檔案:
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>9.3.0</version>
</dependency>
此依賴項為我們提供了從應用程式與 Cloud Firestore 互動所需的類別。
3.2.資料模型
現在,讓我們定義我們的資料模型:
class Task {
public static final String PATH = "tasks";
private String title;
private String description;
private String status;
private Date dueDate;
// standard setters and getters
}
Task
類別是我們教程中的中心實體,代表任務管理應用程式中的task
。
PATH
常數定義了我們將在其中儲存task
文件的 Firestore 集合路徑。
3.3.定義Firestore
配置 Bean
現在,為了與 Cloud Firestore 資料庫交互,我們需要配置私鑰來驗證 API 請求。
為了進行演示,我們將在src/main/resources
目錄中建立private-key.json
檔案。然而,在生產中,應該從環境變數載入私鑰或從秘密管理系統取得私鑰以增強安全性。
我們將使用@Value
註解載入我們的私鑰並使用它來定義我們的Firestore
bean:
@Value("classpath:/private-key.json")
private Resource privateKey;
@Bean
public Firestore firestore() {
InputStream credentials = new ByteArrayInputStream(privateKey.getContentAsByteArray());
FirebaseOptions firebaseOptions = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentials))
.build();
FirebaseApp firebaseApp = FirebaseApp.initializeApp(firebaseOptions);
return FirestoreClient.getFirestore(firebaseApp);
}
Firestore
類別是與 Cloud Firestore 資料庫互動的主要入口點。
4. 使用測試容器設定本機測試環境
為了方便本機開發和測試,我們將使用 Testcontainers 的GCloud 模組來設定 Cloud Firestore 模擬器。為此,我們將其依賴項新增至pom.xml
檔中:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>gcloud</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
透過 Testcontainers 運行 Firestore 模擬器的先決條件是一個活動的 Docker 執行個體。
新增所需的依賴項後,我們將建立一個定義新Firestore
bean 的@TestConfiguration
類別:
private static FirestoreEmulatorContainer firestoreEmulatorContainer = new FirestoreEmulatorContainer(
DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:488.0.0-emulators")
);
@TestConfiguration
static class FirestoreTestConfiguration {
@Bean
public Firestore firestore() {
firestoreEmulatorContainer.start();
FirestoreOptions options = FirestoreOptions
.getDefaultInstance()
.toBuilder()
.setProjectId(RandomString.make().toLowerCase())
.setCredentials(NoCredentials.getInstance())
.setHost(firestoreEmulatorContainer.getEmulatorEndpoint())
.build();
return options.getService();
}
}
我們使用Google Cloud CLI Docker 映像來建立模擬器的容器。然後在firestore()
bean 方法中,我們啟動容器並配置Firestore
bean 以連接到模擬器端點。
此設定允許我們啟動 Cloud Firestore 模擬器的一次性實例,並讓我們的應用程式連接到它而不是實際的 Cloud Firestore 資料庫。
5. 執行CRUD操作
設定好測試環境後,讓我們來探索如何對Task
資料模型執行 CRUD 操作。
5.1.建立文檔
讓我們先建立一個新的task
文件:
Task task = Instancio.create(Task.class);
DocumentReference taskReference = firestore
.collection(Task.PATH)
.document();
taskReference.set(task);
String taskId = taskReference.getId();
assertThat(taskId).isNotBlank();
我們使用 Instancio 使用隨機測試資料建立一個新的Task
物件。然後,我們對tasks
集合呼叫document()
方法來取得DocumentReference
對象,該對象表示文件在 Cloud Firestore 資料庫中的位置。最後,我們在DocumentReference
物件上設定task
資料以建立新的task
文檔。
當我們呼叫不帶任何參數的document()
方法時,Firestore 會自動為我們產生一個唯一的文檔 ID 。我們可以使用getId()
方法來擷取此自動產生的 ID。
或者,我們可以使用自訂 ID 建立task
文件:
Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);
firestore
.collection(Task.PATH)
.document(taskId)
.set(task);
Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
DocumentSnapshot taskSnapshot = firestore
.collection(Task.PATH)
.document(taskId)
.get().get();
assertThat(taskSnapshot.exists())
.isTrue();
});
在這裡,我們產生一個隨機taskId
並將其傳遞給document()
方法以根據它建立一個新的task
文件。然後,我們使用 Awaitility 等待文件建立並斷言其存在。
5.2.檢索和查詢文檔
儘管我們在上一節中間接了解如何透過 ID 檢索task
文檔,但讓我們仔細看看:
Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);
// ... save task in Firestore
DocumentSnapshot taskSnapshot = firestore
.collection(Task.PATH)
.document(taskId)
.get().get();
Task retrievedTask = taskSnapshot.toObject(Task.class);
assertThat(retrievedTask)
.usingRecursiveComparison()
.isEqualTo(task);
為了檢索我們的任務文檔,我們呼叫DocumentReference
物件的get()
方法。此方法傳回一個ApiFuture<DocumentSnapshot>
,表示非同步操作。為了阻止並等待操作完成,我們在傳回的 future 上再次呼叫get()
方法,這為我們提供了一個DocumentSnapshot
物件。
要將DocumentSnapshot
物件轉換為Task
對象,我們使用toObject()
方法。
此外,我們也可以根據特定條件查詢文件:
// Set up test data
Task completedTask = Instancio.of(Task.class)
.set(field(Task::getStatus), "COMPLETED")
.create();
Task inProgressTask = // ... task with status IN_PROGRESS
Task anotherCompletedTask = // ... task with status COMPLETED
List<Task> tasks = List.of(completedTask, inProgressTask, anotherCompletedTask);
// ... save all the tasks in Firestore
// Retrieve completed tasks
List<QueryDocumentSnapshot> retrievedTaskSnapshots = firestore
.collection(Task.PATH)
.whereEqualTo("status", "COMPLETED")
.get().get().getDocuments();
// Verify only matching tasks are retrieved
List<Task> retrievedTasks = retrievedTaskSnapshots
.stream()
.map(snapshot -> snapshot.toObject(Task.class))
.toList();
assertThat(retrievedTasks)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(completedTask, anotherCompletedTask);
在上面的範例中,我們建立了多個具有不同status
值的task
文檔,並將它們儲存到我們的 Cloud Firestore 資料庫中。然後,我們使用whereEqualTo()
方法僅檢索COMPLETED status
task
文件。
另外,我們可以組合多個查詢條件:
List<QueryDocumentSnapshot> retrievedTaskSnapshots = firestore
.collection(Task.PATH)
.whereEqualTo("status", "COMPLETED")
.whereGreaterThanOrEqualTo("dueDate", Date.from(Instant.now()))
.whereLessThanOrEqualTo("dueDate", Date.from(Instant.now().plus(7, ChronoUnit.DAYS)))
.get().get().getDocuments();
在這裡,我們查詢未來 7 天內所有具有dueDate
值的COMPLETED
tasks
。
5.3.更新文件
要更新 Cloud Firestore 中的文檔,我們遵循與建立文檔類似的流程。如果指定的文檔 ID 不存在,Cloud Firestore 會建立一個新文件;否則,它會更新現有文件:
// Save initial task in Firestore
String taskId = Instancio.create(String.class);
Task initialTask = Instancio.of(Task.class)
.set(field(Task::getStatus), "IN_PROGRESS")
.create();
firestore
.collection(Task.PATH)
.document(taskId)
.set(initialTask);
// Update the task
Task updatedTask = initialTask;
updatedTask.setStatus("COMPLETED");
firestore
.collection(Task.PATH)
.document(taskId)
.set(initialTask);
// Verify the task was updated correctly
Task retrievedTask = firestore
.collection(Task.PATH)
.document(taskId)
.get().get()
.toObject(Task.class);
assertThat(retrievedTask)
.usingRecursiveComparison()
.isNotEqualTo(initialTask)
.ignoringFields("status")
.isEqualTo(initialTask);
我們首先建立一個IN_PROGRESS status
新task
文件。然後,我們使用更新後的Task
物件再次呼叫set()
方法,將其status
更新為COMPLETED
。最後,我們從資料庫中取得文件並驗證變更是否正確應用。
5.4.刪除文檔
最後,我們來看看如何刪除我們的task
文件:
// Save task in Firestore
Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);
firestore
.collection(Task.PATH)
.document(taskId)
.set(task);
// Ensure the task is created
Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
DocumentSnapshot taskSnapshot = firestore
.collection(Task.PATH)
.document(taskId)
.get().get();
assertThat(taskSnapshot.exists())
.isTrue();
});
// Delete the task
firestore
.collection(Task.PATH)
.document(taskId)
.delete();
// Assert that the task is deleted
Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
DocumentSnapshot taskSnapshot = firestore
.collection(Task.PATH)
.document(taskId)
.get().get();
assertThat(taskSnapshot.exists())
.isFalse();
});
在這裡,我們首先建立一個新的task
文件並確保其存在。然後,我們呼叫DocumentReference
物件上的delete()
方法來刪除我們的task
並驗證文件是否不再存在。
六、結論
在本文中,我們探索如何使用 Cloud Firestore 在 Spring Boot 應用程式中實現資料持久化。
我們完成了必要的配置,包括使用 Testcontainers 設定本地測試環境,並對我們的task
資料模型執行 CRUD 操作。
與往常一樣,本文中使用的所有程式碼範例都可以在 GitHub 上找到。