This commit is contained in:
2026-02-19 17:50:14 +00:00
parent da95a64fb7
commit 6f629c53a6
171 changed files with 7281 additions and 1144 deletions

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
* Your use of it is subject to your agreement with Google.
*/
package com.example.repository;
import com.example.util.FirestoreTimestampDeserializer;
import com.example.util.FirestoreTimestampSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.QueryDocumentSnapshot;
import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.firestore.WriteBatch;
import com.google.cloud.firestore.WriteResult;
import com.google.cloud.firestore.CollectionReference;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* A base repository for performing low-level operations with Firestore. It provides a generic
* interface for common data access tasks such as getting document references, performing reads,
* writes, and batched updates. This class also handles the serialization and deserialization of
* Java objects to and from Firestore documents using an `ObjectMapper`.
*/
@Repository
public class FirestoreBaseRepository {
private static final Logger logger = LoggerFactory.getLogger(FirestoreBaseRepository.class);
private final Firestore firestore;
private final ObjectMapper objectMapper;
@Value("${app.id:default-app-id}")
private String appId;
public FirestoreBaseRepository(Firestore firestore, ObjectMapper objectMapper) {
this.firestore = firestore;
this.objectMapper = objectMapper;
// Register JavaTimeModule for standard java.time handling
if (!ObjectMapper.findModules().stream().anyMatch(m -> m instanceof JavaTimeModule)) {
objectMapper.registerModule(new JavaTimeModule());
}
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// Register ParameterNamesModule, crucial for Java Records and classes compiled with -parameters
if (!ObjectMapper.findModules().stream().anyMatch(m -> m instanceof ParameterNamesModule)) {
objectMapper.registerModule(new ParameterNamesModule());
}
// These specific Timestamp (Google Cloud) deserializers/serializers are for ObjectMapper
// to handle com.google.cloud.Timestamp objects when mapping other types.
// They are generally not the cause of the Redis deserialization error for Instant.
SimpleModule firestoreTimestampModule = new SimpleModule();
firestoreTimestampModule.addDeserializer(
com.google.cloud.Timestamp.class, new FirestoreTimestampDeserializer());
firestoreTimestampModule.addSerializer(
com.google.cloud.Timestamp.class, new FirestoreTimestampSerializer());
objectMapper.registerModule(firestoreTimestampModule);
logger.info(
"FirestoreBaseRepository initialized with Firestore client and ObjectMapper. App ID will be: {}",
appId);
}
public DocumentReference getDocumentReference(String collectionPath, String documentId) {
Objects.requireNonNull(collectionPath, "Collection path cannot be null.");
Objects.requireNonNull(documentId, "Document ID cannot be null.");
return firestore.collection(collectionPath).document(documentId);
}
public <T> T getDocument(DocumentReference docRef, Class<T> clazz)
throws InterruptedException, ExecutionException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
Objects.requireNonNull(clazz, "Class for mapping cannot be null.");
ApiFuture<DocumentSnapshot> future = docRef.get();
DocumentSnapshot document = future.get();
if (document.exists()) {
try {
logger.debug(
"FirestoreBaseRepository: Raw document data for {}: {}",
docRef.getPath(),
document.getData());
T result = objectMapper.convertValue(document.getData(), clazz);
return result;
} catch (IllegalArgumentException e) {
logger.error(
"Failed to convert Firestore document data to {}: {}", clazz.getName(), e.getMessage(), e);
throw new RuntimeException(
"Failed to convert Firestore document data to " + clazz.getName(), e);
}
}
return null;
}
public DocumentSnapshot getDocumentSnapshot(DocumentReference docRef)
throws ExecutionException, InterruptedException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
ApiFuture<DocumentSnapshot> future = docRef.get();
return future.get();
}
public Flux<DocumentSnapshot> getDocuments(String collectionPath) {
return Flux.create(sink -> {
ApiFuture<QuerySnapshot> future = firestore.collection(collectionPath).get();
future.addListener(() -> {
try {
QuerySnapshot querySnapshot = future.get();
if (querySnapshot != null) {
querySnapshot.getDocuments().forEach(sink::next);
}
sink.complete();
} catch (InterruptedException | ExecutionException e) {
sink.error(e);
}
}, Runnable::run);
});
}
public Mono<DocumentSnapshot> getDocumentsByField(
String collectionPath, String fieldName, String value) {
return Mono.fromCallable(
() -> {
Query query = firestore.collection(collectionPath).whereEqualTo(fieldName, value);
ApiFuture<QuerySnapshot> future = query.get();
QuerySnapshot querySnapshot = future.get();
if (!querySnapshot.isEmpty()) {
return querySnapshot.getDocuments().get(0);
}
return null;
});
}
public boolean documentExists(DocumentReference docRef)
throws InterruptedException, ExecutionException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
ApiFuture<DocumentSnapshot> future = docRef.get();
return future.get().exists();
}
public void setDocument(DocumentReference docRef, Object data)
throws InterruptedException, ExecutionException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
Objects.requireNonNull(data, "Data for setting document cannot be null.");
ApiFuture<WriteResult> future = docRef.set(data);
WriteResult writeResult = future.get();
logger.debug(
"Document set: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
}
public void updateDocument(DocumentReference docRef, Map<String, Object> updates)
throws InterruptedException, ExecutionException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
Objects.requireNonNull(updates, "Updates map cannot be null.");
ApiFuture<WriteResult> future = docRef.update(updates);
WriteResult writeResult = future.get();
logger.debug(
"Document updated: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
}
public void deleteDocument(DocumentReference docRef)
throws InterruptedException, ExecutionException {
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
ApiFuture<WriteResult> future = docRef.delete();
WriteResult writeResult = future.get();
logger.debug(
"Document deleted: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
}
public WriteBatch createBatch() {
return firestore.batch();
}
public void commitBatch(WriteBatch batch) throws InterruptedException, ExecutionException {
Objects.requireNonNull(batch, "WriteBatch cannot be null.");
batch.commit().get();
logger.debug("Batch committed successfully.");
}
public String getAppId() {
return appId;
}
public void deleteCollection(String collectionPath, int batchSize) {
try {
CollectionReference collection = firestore.collection(collectionPath);
ApiFuture<QuerySnapshot> future = collection.limit(batchSize).get();
int deleted = 0;
// future.get() blocks on document retrieval
List<QueryDocumentSnapshot> documents = future.get().getDocuments();
while (!documents.isEmpty()) {
for (QueryDocumentSnapshot document : documents) {
document.getReference().delete();
++deleted;
}
future = collection.limit(batchSize).get();
documents = future.get().getDocuments();
}
logger.info("Deleted {} documents from collection {}", deleted, collectionPath);
} catch (Exception e) {
logger.error("Error deleting collection: " + e.getMessage(), e);
throw new RuntimeException("Error deleting collection", e);
}
}
public void deleteDocumentAndSubcollections(DocumentReference docRef, String subcollection)
throws ExecutionException, InterruptedException {
deleteCollection(docRef.collection(subcollection).getPath(), 50);
deleteDocument(docRef);
}
}