Initial Python rewrite
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user