Як я інтегрував Firestore у багатокористувацький мікросервіс на Spring Boot (з нестандартною схемою)

pic

Вступ

Нещодавно я зіткнувся з викликом інтеграції Google Cloud Firestore у мультитенантну мікросервісну архітектуру на Spring Boot. Версія бібліотеки GCP, яку я використовував, була досить недостатньо документована, особливо для моїх конкретних потреб:

• Кілька шкіл (тенантів) в одному проекті Firestore

Невизначена схема з підколекціями, динамічними полями JSON та вкладеними даними

• Необхідність перевизначити стандартну поведінку мапінгу Firestore для отримання правильних імен колекцій

У цій статті я проведу вас через:

  1. Чому ми вибрали Firestore для частини нашого потоку даних

  2. Як я перевизначив стандартні налаштування бібліотеки для мультитенантності

  3. Що знадобилось для обробки підколекцій та динамічного JSON

  4. Де допомогла власна утиліта FirestoreUtils для мосту асинхронних операцій

Сподіваюся, це буде корисно для тих, хто намагається зробити щось подібне у своєму мікросервісі на Spring Boot.

Сценарій використання: Зберігання даних учителів та класів по школах (не реальний сценарій)

У нашому сценарії кожна школа керує даними про учителів в Firestore. Кожен учитель може мати підколекції для класів або зберігати інформацію про класи в вкладеному JSON-об'єкті всередині документа учителя. Замість того, щоб постійно оновлювати реляційну базу даних, ми вирішили:

Дати системі можливість читати/писати ці документи учителів безпосередньо в Firestore

• Тільки синхронізувати дані про учителів/класи з нашим мікросервісом на певних етапах (наприклад, наприкінці семестру)

Гнучкість та масштабованість Firestore роблять його чудовим кандидатом для високошвидкісних записів та доступу до даних. Однак стандартна інтеграція Spring Data Firestore не підтримує:

Мультитенантність: нам потрібен підхід для кожної школи без створення кількох проектів Firestore

Підколекції та вкладений JSON: це більш складно, ніж простий сценарій відображення поля в POJO

  • Перевизначення імен колекцій: ми хотіли імена типу teacherDataSCHOOLABC або teacherDataSCHOOLXYZ

Крок 1: Налаштування залежностей

Додайте залежності GCP до секції Dependency Management у вашому файлі POM.xml



 com.google.cloud  
 spring-cloud-gcp-dependencies  
 5.8.0  
 pom  
 import  



потім додайте spring-cloud-gcp-starter-data-firestore до секції dependencies у вашому файлі POM.xml



 com.google.cloud  
 spring-cloud-gcp-starter-data-firestore  


Крок 2: Зробимо це мультитенантним

Динамічні імена колекцій

Оскільки кожен запит до нашого мікросервісу містить ID школи в заголовку, ми хочемо зберігати дані кожної школи окремо, наприклад:

teacherData_SCHOOL1  
teacherData_SCHOOL2

Перевизначення FirestoreMappingContext

За замовчуванням Spring Data Firestore очікує фіксоване ім’я колекції від @Document. Нам потрібно динамічно додавати _ до кожного імені колекції. Після вивчення вихідного коду бібліотеки та прочитання GitHub-обговорень, я з’ясував, що можна:

  1. Розширити FirestoreMappingContext

  2. Перевизначити логіку, що визначає імена колекцій

3.
Додавати або попередньо додавати ID школи під час виконання

Цей підхід, хоча й дещо «хаки», надав кожній школі свою власну колекцію Firestore без необхідності створювати кілька проектів Firestore.

Крок 3: Підколекції та Вкладений JSON

Учителі зберігали класи в підколекціях:

/teacherData_{schoolId}/{teacherId}/classes/{classId}

а дані зберігаються в динамічних полях JSON, що вбудовані в документ класу — наприклад:

{  
 "BIO101": {  
 "room": "B-10",  
 "currentEnrollment": 22  
 },  
 "MATH202": {  
 "room": "A-3",  
 "currentEnrollment": 30  
 }  
}

Чому власний репозиторій?

Підколекції та динамічні поля не підходять для стандартного підходу репозиторіїв Spring Data Firestore

• Нам потрібна була гнучкість для ручного побудови шляхів та обробки незвичних структур даних

• Ми хотіли мати більший контроль над пакетними операціями та паралельністю

Тому найкращим рішенням став власний репозиторій, який безпосередньо використовує бін Firestore.

Крок 4: Міст між ApiFuture Firestore та FirestoreUtils

При роботі з Firestore в Java багато операцій повертають об'єкт com.google.api.core.ApiFuture, який не є тим самим, що й вбудований в Java CompletableFuture. Для кращої інтеграції з Spring та більш плавної обробки асинхронних викликів я створив невеликий утилітний клас:

package io.osharif.school.config.multitenant.firestore;  

import com.google.api.core.ApiFuture;  
import com.google.common.util.concurrent.MoreExecutors;  
import java.util.concurrent.CompletableFuture;  

public class FirestoreUtils {  
 public static  CompletableFuture toCompletableFuture(ApiFuture apiFuture) {  
 CompletableFuture completableFuture = new CompletableFuture<>();  

 apiFuture.addListener(() -> {  
 try {  
 // Отримати результат з ApiFuture та завершити CompletableFuture  
 T result = apiFuture.get();  
 completableFuture.complete(result);  
 } catch (Throwable t) {  
 // Якщо сталася помилка, завершити CompletableFuture з помилкою  
 completableFuture.completeExceptionally(t);  
 }  
 }, MoreExecutors.directExecutor());  

 return completableFuture;  
 }  
}

Цей метод перетворює ApiFuture на CompletableFuture, дозволяючи нам використовувати більш звичні для Java патерни паралельності та плавно обробляти дані чи помилки. Ми також використовуємо MoreExecutors.directExecutor() для виконання слухача в тому ж контексті потоку, якщо це необхідно.

Приклад використання у власному репозиторії

ApiFuture future = teacherCollection.get();  
CompletableFuture completableFuture = FirestoreUtils.toCompletableFuture(future);  

return completableFuture  
 .thenApply(querySnapshot -> querySnapshot.getDocuments().stream()  
 .map(document -> customMapper.mapToTeacher(document.getData()))  
 .collect(Collectors.toList())  
 );

Це дозволяє зберігати наш код узгодженим зі стандартними примітивами паралельності Java.

Крок 5: Власний мапер для складних документів

З динамічними ключами JSON або посиланнями на підколекції стандартний конвертер Firestore не працює. Тому я створив клас CustomMapper:

  1. Ітерує по кожному ключу в JSON

  2. Тлумачить цей ключ як унікальний клас або запис даних

3.
Конвертує вкладений об'єкт у POJO (ClassData тощо)

Спрощена версія може виглядати ось так:

public class CustomMapper {  

 public TeacherData mapToTeacher(Map data) {  
 TeacherData teacher = new TeacherData();  
 Map classesMap = new HashMap<>();  

 for (Map.Entry entry : data.entrySet()) {  
 if (entry.getValue() instanceof Map) {  
 ClassData classData = mapClassData((Map) entry.getValue());  
 classesMap.put(entry.getKey(), classData);  
 }  
 }  

 teacher.setClasses(classesMap);  
 return teacher;  
 }  

 private ClassData mapClassData(Map classMap) {  
 // Конвертуємо поля карти в об'єкт ClassData  
 ClassData cd = new ClassData();  
 cd.setRoom((String) classMap.get("room"));  
 cd.setCurrentEnrollment(((Number) classMap.get("currentEnrollment")).intValue());  
 // ... більше полів, якщо потрібно ...  
 return cd;  
 }  
}

Коли ми записуємо дані назад у Firestore, я роблю зворотній процес: конвертую TeacherData в карту, яка відповідає динамічній структурі.

Крок 6: Пакетні операції

Коли потрібно оновити багато вчителів або класів одночасно, виклик Firestore документ за документом може призвести до проблеми N+1 з продуктивністю. WriteBatch у Firestore вирішує цю проблему, дозволяючи нам поставити кілька операцій у чергу і виконати їх всі за один раз.

public CompletableFuture updateAllTeachers(String schoolId, List teachers) {  
 String collectionPath = buildTeacherCollectionPath(schoolId);  
 CollectionReference teacherCollection = firestore.collection(collectionPath);  

 WriteBatch batch = firestore.batch();  

 for (TeacherData teacher : teachers) {  
 DocumentReference docRef = teacherCollection.document(teacher.getId());  
 Map teacherDataMap = customMapper.teacherToMap(teacher);  
 batch.update(docRef, teacherDataMap);  
 }  

 ApiFuture> future = batch.commit();  
 return FirestoreUtils.toCompletableFuture(future)  
 .thenApply(writeResults -> {  
 // Опційно логувати або обробляти результати  
 return null; // Повернення не потрібне  
 });  
}

Ми також можемо поділити великі списки, щоб дотримуватись ліміту Firestore на 500 операцій за пакет.

Повна реалізація власного репозиторію для великої картини

package io.osharif.school.data.firestore.repositories;  

import com.google.api.core.ApiFuture;  
import com.google.cloud.firestore.CollectionReference;  
import com.google.cloud.firestore.DocumentReference;  
import com.google.cloud.firestore.Firestore;  
import com.google.cloud.firestore.QuerySnapshot;  
import com.google.cloud.firestore.WriteBatch;  
import com.google.cloud.firestore.WriteResult;  
import com.google.common.util.concurrent.MoreExecutors;  
import io.osharif.school.config.multitenant.firestore.CustomMapper;  
import io.osharif.school.config.multitenant.firestore.FirestoreUtils;  
import io.osharif.school.data.firestore.FirestoreClass;  
import io.osharif.school.data.firestore.FirestoreTeacher;  
import io.osharif.school.enums.FirestoreCollections;  
import io.osharif.school.services.TenantContext; // або SchoolContext, якщо ви його перейменували  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Repository;  
import reactor.core.publisher.Flux;  
import reactor.core.publisher.Mono;  

import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.concurrent.CompletableFuture;  

/**  
 * Демонструє репозиторій Firestore для мульти-орендарів для документів "Teacher"  
 * та їх підколекції документів "Class". Повторює ваш оригінальний підхід.  
 */  
@Repository  
@Slf4j  
public class FirestoreSchoolCustomRepoImpl {  

 @Autowired  
 private Firestore firestore;  

 @Autowired  
 private CustomMapper customFirestoreClassMapper;  

 /**  
 * Отримує всі класи для конкретної школи та вчителя, використовуючи підколекції.  
 */

*  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherId Ідентифікатор вчителя.  
 * @return Flux, який випромінює сутності FirestoreClass.  
 */  
 public Flux getClasses(String schoolId, String teacherId) {  
 String collectionPath = buildClassCollectionPath(schoolId, teacherId);  
 CollectionReference classesCollection = firestore.collection(collectionPath);  

 ApiFuture future = classesCollection.get();  

 return Mono.create(sink -> {  
 future.addListener(() -> {  
 try {  
 QuerySnapshot querySnapshot = future.get();  
 sink.success(querySnapshot);  
 } catch (Throwable t) {  
 sink.error(t);  
 }  
 }, MoreExecutors.directExecutor());  
 })  
 .flatMapMany(querySnapshot -> Flux.fromIterable(querySnapshot.getDocuments()))  
 .map(documentSnapshot -> {  
 String classId = documentSnapshot.getId();  
 // Перетворюємо документ Firestore -> сутність FirestoreClass  
 FirestoreClass clazz = customFirestoreClassMapper.mapToEntity(  
 documentSnapshot.getData(),  
 FirestoreClass.class  
 );  
 clazz.setId(classId);  
 return clazz;  
 });  
 }  

 /**  
 * Отримує всіх вчителів для заданої школи, кожен з яких може мати підколекції (класи).  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @return Flux, який випромінює сутності FirestoreTeacher.  
 */  
 public Flux getTeachers(String schoolId) {  
 String collectionPath = buildTeachersCollectionPath(schoolId);  
 CollectionReference teachersCollection = firestore.collection(collectionPath);  

 ApiFuture future = teachersCollection.get();  

 return Mono.create(sink -> {  
 future.addListener(() -> {  
 try {  
 QuerySnapshot querySnapshot = future.get();  
 sink.success(querySnapshot);  
 } catch (Throwable t) {  
 sink.error(t);  
 }  
 }, MoreExecutors.directExecutor());  
 })  
 .flatMapMany(querySnapshot -> Flux.fromIterable(querySnapshot.getDocuments()))  
 .map(documentSnapshot -> {  
 String teacherId = documentSnapshot.getId();  
 // Приклад використання власного маппера  
 FirestoreTeacher teacher = customFirestoreClassMapper.mapTeacher(documentSnapshot);  
 teacher.setId(teacherId);  
 return teacher;  
 });  
 }  

 /**  
 * Масове оновлення кількох класів для конкретного вчителя з використанням WriteBatch Firestore.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherId Ідентифікатор вчителя.  
 * @param classes Список сутностей FirestoreClass, які потрібно оновити.  
 * @return Mono, що сигналізує про завершення операції.  
 */

*/  
 public Mono updateAllClasses(String schoolId, String teacherId, List classes) {  
 if (classes == null || classes.isEmpty()) {  
 return Mono.empty(); // Немає що оновлювати  
 }  

 String collectionPath = buildClassCollectionPath(schoolId, teacherId);  
 CollectionReference classesCollection = firestore.collection(collectionPath);  

 // Запис у batch Firestore може мати максимум 500 операцій на кожен пакет  
 int batchSize = 500;  
 List> classPartitions = partitionList(classes, batchSize);  

 List> batchMonos = new ArrayList<>();  

 for (List classBatch : classPartitions) {  
 WriteBatch batch = firestore.batch();  

 for (FirestoreClass clazz : classBatch) {  
 String classId = clazz.getId();  
 if (classId == null || classId.isEmpty()) {  
 log.warn("Ідентифікатор класу відсутній або порожній для класу: {}", clazz);  
 continue; // Пропустити некоректні ID документів  
 }  

 DocumentReference classRef = classesCollection.document(classId);  
 Map classData = customFirestoreClassMapper.entityToMap(clazz);  

 batch.update(classRef, classData);  
 }  

 // Комітуємо пакет  
 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 Mono batchMono = Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно оновлено {} класів в одному пакеті.", writeResults.size());  
 })  
 .doOnError(error -> {  
 log.error("Помилка при оновленні пакету: {}", error.getMessage());  
 })  
 .then();  

 batchMonos.add(batchMono);  
 }  

 // Об'єднуємо всі операції пакетів  
 return Mono.when(batchMonos);  
 }  

 /**  
 * Масове оновлення кількох вчителів з використанням WriteBatch Firestore.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teachers Список сутностей FirestoreTeacher, які потрібно оновити.  
 * @return Mono, що сигналізує про завершення операції.  
 */  
 public Mono updateAllTeachers(String schoolId, List teachers) {  
 if (teachers == null || teachers.isEmpty()) {  
 return Mono.empty(); // Немає що оновлювати  
 }  

 String collectionPath = buildTeachersCollectionPath(schoolId);  
 CollectionReference teachersCollection = firestore.collection(collectionPath);  

 int batchSize = 500;  
 List> teacherPartitions = partitionList(teachers, batchSize);  

 List> batchMonos = new ArrayList<>();  

 for (List teacherBatch : teacherPartitions) {  
 WriteBatch batch = firestore.batch();  

 for (FirestoreTeacher teacher : teacherBatch) {  
 String teacherId = teacher.getId();  
 if (teacherId == null || teacherId.isEmpty()) {  
 log.warn("Ідентифікатор вчителя відсутній або порожній для вчителя: {}", teacher);  
 continue;  
 }  

 DocumentReference teacherRef = teachersCollection.document(teacherId);  
 Map teacherData = customFirestoreClassMapper.entityToMap(teacher);  

 batch.update(teacherRef, teacherData);  
 }  

 // Комітуємо пакет  
 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 Mono batchMono = Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно оновлено {} вчителів в одному пакеті.", writeResults.size());  
 })  
 .doOnError(error -> {  
 log.error("Помилка при оновленні пакету вчителів: {}", error.getMessage());  
 })  
 .then();  

 batchMonos.add(batchMono);  
 }  

 return Mono.when(batchMonos);  
 }  

 /**  
 * Видаляє окремий документ класу.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherId Ідентифікатор вчителя.  
 * @param classId Ідентифікатор документа класу, який потрібно видалити.  
 * @return Mono, що сигналізує про завершення операції.  
 */

*/  
 public Mono deleteClass(String schoolId, String teacherId, String classId) {  
 if (classId == null || classId.isEmpty()) {  
 return Mono.error(new IllegalArgumentException("Ідентифікатор класу відсутній або порожній"));  
 }  

 String collectionPath = buildClassCollectionPath(schoolId, teacherId);  
 CollectionReference classesCollection = firestore.collection(collectionPath);  
 DocumentReference classRef = classesCollection.document(classId);  

 WriteBatch batch = firestore.batch();  
 batch.delete(classRef);  

 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 return Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно видалено клас з ідентифікатором: {}", classId);  
 })  
 .doOnError(error -> {  
 log.error("Помилка при видаленні класу з ідентифікатором {}: {}", classId, error.getMessage());  
 })  
 .then();  
 }  

 /**  
 * Видаляє кілька класів для конкретного вчителя.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherId Ідентифікатор вчителя.  
 * @param classIds Список ідентифікаторів класів для видалення.  
 */  
 public void deleteClasses(String schoolId, String teacherId, List classIds) {  
 if (classIds == null || classIds.isEmpty()) {  
 return; // Немає що видаляти  
 }  

 String collectionPath = buildClassCollectionPath(schoolId, teacherId);  
 CollectionReference classesCollection = firestore.collection(collectionPath);  

 int batchSize = 500;  
 List> classIdPartitions = partitionList(classIds, batchSize);  

 List> batchMonos = new ArrayList<>();  

 for (List classIdBatch : classIdPartitions) {  
 WriteBatch batch = firestore.batch();  

 for (String classId : classIdBatch) {  
 if (classId == null || classId.isEmpty()) {  
 log.warn("Ідентифікатор класу відсутній або порожній, пропускаємо видалення.");  
 continue;  
 }  

 DocumentReference classRef = classesCollection.document(classId);  
 batch.delete(classRef);  
 }  

 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 Mono batchMono = Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно видалено {} класів в одному пакеті.", writeResults.size());  
 })  
 .doOnError(error -> {  
 log.error("Помилка під час масового видалення класів: {}", error.getMessage());  
 })  
 .then();  

 batchMonos.add(batchMono);  
 }  

 // Об'єднуємо, але не підписуємось — коригуйте, якщо хочете асинхронний потік або блокування  
 Mono.when(batchMonos);  
 }  

 /**  
 * Видаляє одного вчителя.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherId Ідентифікатор документа вчителя для видалення.  
 */  
 public void deleteTeacher(String schoolId, String teacherId) {  
 if (teacherId == null || teacherId.isEmpty()) {  
 Mono.error(new IllegalArgumentException("Ідентифікатор вчителя відсутній або порожній"));  
 return;  
 }  

 String collectionPath = buildTeachersCollectionPath(schoolId);  
 CollectionReference teachersCollection = firestore.collection(collectionPath);  
 DocumentReference teacherRef = teachersCollection.document(teacherId);  

 WriteBatch batch = firestore.batch();  
 batch.delete(teacherRef);  

 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно видалено вчителя з ідентифікатором: {}", teacherId);  
 })  
 .doOnError(error -> {  
 log.error("Помилка при видаленні вчителя з ідентифікатором {}: {}", teacherId, error.getMessage());  
 })  
 .then();  
 }  

 /**  
 * Видаляє кількох вчителів за їхніми ідентифікаторами.  
 *  
 * @param schoolId Ідентифікатор школи (орендаря).  
 * @param teacherIds Список ідентифікаторів вчителів для видалення.  
 * @return Mono, що сигналізує про завершення операції.  
 */

*/  
 public Mono deleteTeachers(String schoolId, List teacherIds) {  
 if (teacherIds == null || teacherIds.isEmpty()) {  
 return Mono.empty();  
 }  

 String collectionPath = buildTeachersCollectionPath(schoolId);  
 CollectionReference teachersCollection = firestore.collection(collectionPath);  

 int batchSize = 500;  
 List> teacherIdPartitions = partitionList(teacherIds, batchSize);  

 List> batchMonos = new ArrayList<>();  

 for (List teacherIdBatch : teacherIdPartitions) {  
 WriteBatch batch = firestore.batch();  

 for (String tid : teacherIdBatch) {  
 if (tid == null || tid.isEmpty()) {  
 log.warn("Ідентифікатор вчителя відсутній або порожній, пропускаємо видалення.");  
 continue;  
 }  

 DocumentReference teacherRef = teachersCollection.document(tid);  
 batch.delete(teacherRef);  
 }  

 ApiFuture> future = batch.commit();  
 CompletableFuture> completableFuture = FirestoreUtils.toCompletableFuture(future);  

 Mono batchMono = Mono.fromFuture(completableFuture)  
 .doOnSuccess(writeResults -> {  
 log.info("Успішно видалено {} вчителів в одному пакеті.", writeResults.size());  
 })  
 .doOnError(error -> {  
 log.error("Помилка під час масового видалення вчителів: {}", error.getMessage());  
 })  
 .then();  

 batchMonos.add(batchMono);  
 }  

 return Mono.when(batchMonos);  
 }  

 /**  
 * Допоміжний метод для розбиття списку на підсписки вказаного розміру.  
 */  
 private  List> partitionList(List list, int size) {  
 List> partitions = new ArrayList<>();  
 if (list == null || list.isEmpty()) {  
 return partitions;  
 }  
 for (int i = 0; i < list.size(); i += size) {  
 partitions.add(new ArrayList<>(list.subList(i, Math.min(i + size, list.size()))));  
 }  
 return partitions;  
 }  

 /**  
 * Створює шлях до колекції Firestore для вчителів, наприклад "SCHOOL_DATA_/schoolId/TEACHERS".  
 */  
 private String buildTeachersCollectionPath(String schoolId) {  
 // Приклад: "SCHOOL_DATA_/{schoolId}/TEACHERS"  
 return String.format("%s/%s/%s",  
 FirestoreCollections.SCHOOL_DATA.getName() + "_" + TenantContext.getTenantId().toUpperCase(),  
 schoolId,  
 FirestoreCollections.TEACHERS.getName()  
 );  
 }  

 /**  
 * Створює шлях до колекції Firestore для класів як підколекцію під документом вчителя.  
 */  
 private String buildClassCollectionPath(String schoolId, String teacherId) {  
 // Використовуємо шлях до колекції вчителів, потім додаємо teacherId та CLASSES  
 return String.format("%s/%s/%s",  
 buildTeachersCollectionPath(schoolId),  
 teacherId,  
 FirestoreCollections.CLASSES.getName()  
 );  
 }  
}
public enum FirestoreCollections {  
 SCHOOL_DATA("school_data"),  
 TEACHERS("teachers"),  
 CLASSES("classes");  

 private final String name;  

 FirestoreCollections(String name) {  
 this.name = name;  
 }  

 public String getName() {  
 return name;  
 }  
}

Результати та ключові висновки

  1. Мультиорендність (Multitenancy): Перевизначення FirestoreMappingContext дозволяє елегантно відокремлювати дані кожної школи в окрему колекцію Firestore без потреби створення кількох проектів Firestore.

  2. Обробка підколекцій (Sub-collection Handling): Користувацькі репозиторії дозволяють будувати правильні шляхи Firestore і збирати дані з вкладених колекцій.

  3. Асинхронні утиліти (Async Utility): Клас FirestoreUtils дозволив плавно перейти від ApiFuture до CompletableFuture, що спростило логіку паралелізму і зробило її більш звичною для розробників Java.

  4. Користувацькі мапери (Custom Mappers): Незвичайні структури даних вимагають явної логіки відображення — іншого виходу немає.

  5. Продуктивність (Performance): Використання WriteBatch (або BulkWriter) для операцій з великими обсягами забезпечує уникнення надмірних мережевих витрат.

Остаточні думки

Інтеграція Firestore в мультиорендний (multitenant) мікросервіс на Spring Boot — особливо, коли мова йде про динамічні JSON структури (dynamic JSON structures) — це не просте завдання.
Від перевизначення контексту відображення бібліотеки до кастомних репозиторіїв і маперів (mappers), часто доводиться зібирати рішення, що виходять за межі офіційної документації.

Однак, після налаштування, гнучкість і масштабованість Firestore робить його ідеальним бекендом для певних сценаріїв з високою пропускною здатністю і часто змінюваними даними. Якщо ви стикаєтесь з подібною проблемою або маєте запитання, не соромтесь коментувати нижче або зв’язатися через соціальні мережі — сподіваюсь, цей посібник допоможе вам заощадити кілька нічних годин на налагодження!

Дякую за прочитання та успіхів у програмуванні!

Перекладено з: How I Integrated Firestore Into a Multitenant Spring Boot Microservice (With an Unconventional Schema)

Leave a Reply

Your email address will not be published. Required fields are marked *