Если вы уже используете Google Cloud Firestore , вы, вероятно, знаете его модель данных и вы заметили, что Вложенные коллекции являются одной из самых интересных функций. Я впервые встретился с ними во время работы над моим личным домашним проектом (проект, в котором я могу экспериментировать с новыми технологиями и методологиями).
Итак, я подумал, есть ли способ адаптировать шаблон Репозитория для работы с вложенными коллекциями Firestore. Основной аспект, требующий решения, заключался в том, что в шаблоне репозитория каждый репозиторий обычно связан с определенной моделью, другими словами, он связан с типом. Но чтение со страницы документации Google :
Вложенная коллекция – это коллекция, связанная с определенным документом.
Это означает, что для вложенной коллекции хранилище связано не с типом, а с конкретным Документом .
Давайте посмотрим на результаты этого эксперимента… 🔬
Сценарий
Чтобы понять суть, представьте себе этот практический пример: нам приходится иметь дело с “Домами”, и в каждом из них есть несколько “Комнат” (ванная комната, кухня,…). В каждой комнате есть своя “Мебель” (кровать, два стула и так далее). Чтобы смоделировать этот сценарий, мы можем представить себе реализацию этих сущностей:
- Предмет Мебели
- Комната
- Дом
и они будут связаны таким образом:
Houses <--- a collection | |--- My-House <---a Document of Houses |--Address <---- a field of "My-House" document |--Rooms <----- a Subcollection of "My-House" | |--Kitchen |--Bathroom |--Bedroom <--- a document of Rooms | |--FurnitureItems <--- a Subcollection of Bedroom document | |-- Bed |-- Wardrobe
Хранилище
Для простоты давайте сосредоточимся на этих двух методах
- “Добавить” добавляет документ в коллекцию
- “GetById” возвращает документ по его идентификатору
Говоря с точки зрения интерфейса, у нас есть:
public interface IRepository{ Either Add(T data); Either GetById(String id); }
Игнорируя на мгновение вложенные коллекции, реализующий класс может быть:
public class FirestoreRepositoryimplements IRepository { private CollectionReference collection; private Class classType; public FirestoreRepository(Firestore firestore, String collectionPath, Class classType){ this.collection = firestore.collection(collectionPath); this.classType = classType; } public Either Save(T data){ try{ ApiFuture futureResult = collection.add(data).get().get(); DocumentSnapshot result = futureResult.get(); return Either.right(result.getId()); }catch(Exception ex){ return Either.left(ex.getMessage()); } } public Either GetById(String id) { try{ ApiFuture futureEntry = collection.document(id).get(); DocumentSnapshot entry = futureEntry.get(); return entry.exists() ? Either.right(entry.toObject(classType)) : Either.left("Data not found for id: "+id); }catch(Exception ex){ return Either.left(ex.getMessage()); } } }
Примечание: Методы Api Google для получения моментального снимка документа являются асинхронными, они действительно возвращают Функция Api (реализация Google API будущего java).
Для простоты я решил избегать асинхронного подхода, блокирующего выполнение до тех пор, пока будущее не будет завершено. Да, я знаю, это не очень хорошая идея для реальных проектов 🤓
Приведенная выше реализация предполагает, что вся коллекция находится на корневом уровне, в конструкторе ссылка на коллекцию действительно создается из объекта firestore:
this.collection = firestore.collection(collectionPath);
Это предположение неверно, когда мы используем вложенные коллекции firestore. Вложенная коллекция связана с конкретным документом, поэтому нам нужен a Ссылка на документ для получения одной из его коллекций. Например:
CollectionReference furnitureCollection = firestore .collection("Houses") .document("My-House") .collection("Rooms") .document("bathroom") .collection("FurnitureItems");
Мы замечаем, что нам нужно связать Ссылки на коллекции и Ссылка на документ для доступа к нужной вложенной коллекции.
Итак, во-первых, нам нужно изменить конструктор нашего репозитория Firestore, чтобы принять Ссылку на коллекцию .
public FirestoreRepository(CollectionReference collection, ClassclassType){ this.collection = collection; this.classType = classType; }
Таким образом, мы устранили предположение о том, что каждая коллекция находится на корневом уровне.
Фабрика-хранилище
На этом этапе позвольте мне представить RepositoryFactory чья ответственность заключается:
- цепочка Ссылки на коллекции и Ссылки на документы
- создайте экземпляр репозитория для определенной коллекции (или вложенной коллекции).
Например, если мы хотим получить хранилище для “Предметов мебели” документа “ванная комната” относительно “Мой дом”, мы ожидаем получить что-то вроде:
IRepositoryfurnitureRepo = repoFactory .FromDocument(House.class, "My-House") .FromDocument(Room.class, "bathroom") .GetRepository(FurnitureItem.class); furnitureRepo.Add(new FurnitureItem("chair"));
В противном случае, если мы хотим выполнить запрос через коллекцию House , мы должны иметь:
IRepositoryhouseRepo = repoFactory.GetRepository(FurnitureItem.class); houseRepo.Add(new House());
Мы замечаем, что метод Из документа должен быть возвращен новый экземпляр RepositoryFactory. Это позволяет нам сохранить ссылку RepositoryFactory для доступа ко всем его вложенным коллекциям. Например, рассмотрим, что в “Доме”, в дополнение к коллекции “Комнат”, есть коллекция “Жильцы”. В этом сценарии мы можем написать:
RepositoryFactory myHouseRepoFactory = repoFactory.FromDocument(House.class, "MyHouse"); myHouseRepoFactory .FromDocument(Room.class, "kitchen") //this do not modify the collection root of myHouseRepoFactory .GetRepository(FurnitureItem.class) .Add(new FurnitureItem()); myHouseRepoFactory .GetRepository(Tenant.class) .Add(new Tenant());
Дизайн репозитория Factory 📐 ✏️
Принимая во внимание все, наши RepositoryFactory потребности:
- Карта для связи типа класса модели с именем коллекции
- Указатель для создания репозитория: если репозиторий находится на корневом уровне, то это должен быть экземпляр firestore, Ссылка на документ в противном случае.
- Способ перемещения указателя на следующий Ссылка на документ возвращает новый экземпляр RepositoryFactory .
- Способ получения экземпляра Репозитория для указанной коллекции (или Вложенной коллекции )
Карта Коллекции Моделей
Чтобы связать каждый тип класса модели с именем соответствующей коллекции, мы используем такую карту:
private final static Map, String> collectionMap = Map.of( House.class, "Houses", Room.class, "Rooms", FurnitureItem.class, "Furniture" );
Указатель
Имейте в виду, что у нас есть два вида указателей:
- Экземпляр Firestore если коллекция находится на корневом уровне
- A Ссылка на документ если коллекция относится к определенному документу (он же Вложенная коллекция)
Мы могли бы сохранить ссылку на Firestore и одну на Ссылка на документ , тогда у нас должна быть некоторая логика, чтобы выбрать, какой из них использовать.
public class RepositoryFactory{ .... private Firestore firestore; private DocumentRefernce documentReference; private Boolean isRootLevel; public RepositoryFactory(Firestore firestore){ this.firestore = firestore; isRootLevel = true; } private RepositoryFactory(DocumentReference documentReference){ this.documentReference = documentReference; isRootLevel = false; } .... private CollectionReference getCollectionReference(String collectionName){ return isRootLevel ? firestore.collection(collectionName) : documentReference.collection(collectionName); } .... }
Но при перемещении указателя с корневого уровня на уровень документа ссылка на хранилище файлов становится бесполезной. Есть ли способ сохранить только тот указатель, который нам нужен?
💡 Функциональный подход к спасению!
Мы действительно могли бы сохранить только ссылку на функцию, которая, учитывая имя коллекции, извлекает Ссылку на коллекцию :
public class RepositoryFactory{ .... private FunctiongetCollectionReference; .... public RepositoryFactory(Firestore firestore){ getCollectionReference = collectionPath -> firestore.collection(collectionPath); } private RepositoryFactory(DocumentReference document){ getCollectionReference = collectionPath -> document.collection(collectionPath); } .... }
Таким образом, каждый экземпляр RepositoryFactory сохраняет только ссылку на правый указатель.
Переместите указатель на определенный документ
Для перемещения по Ссылкам на документы мы не хотим изменять существующий экземпляр RepositoryFactory , но мы должны создать новый экземпляр с новым Ссылка на документ в качестве указателя:
public RepositoryFactory FromDocument(Class> classType, String documentId){ String collectionName = collectionMap.get(classType); DocumentReference documentReference = getCollectionReference.apply(collectionName).document(documentId); return new RepositoryFactory(documentReference); }
Создайте репозиторий для данной модели
На этом этапе нам нужен метод, который, учитывая тип класса модели, возвращает экземпляр Репозитория<> :
publicIRepository GetRepository(Class classType){ String collectionName = collectionMap.get(classType); return new FirestoreRepository (getCollectionReference.apply(collectionName), classType); }
Собери все по кусочкам
Наконец, наша реализация шаблона репозитория должна быть примерно такой:
//IRepository.java public interface IRepository{ Either Add(T data); Either GetById(String id); } //FirestoreRepository.java public class FirestoreRepository implements IRepository { private CollectionReference collection; private Class classType; public FirestoreRepository(CollectionReference collection, Class classType){ this.collection = collection; this.classType = classType; } public Either Add(T data){ try{ DocumentSnapshot result = collection.add(data).get().get().get(); return Either.right(result.getId()); }catch(Exception ex){ return Either.left(ex.getMessage()); } } public Either GetById(String id) { try{ DocumentSnapshot entry = collection.document(id).get().get(); return entry.exists() ? Either.right(entry.toObject(classType)) : Either.left("Data not found for id: "+id); }catch(Exception ex){ return Either.left(ex.getMessage()); } } } //RepositoryFactory.java public class RepositoryFactory{ private Function getCollectionReference; private final static Map , String> collectionMap = Map.of( House.class, "Houses", Room.class, "Rooms", FurnitureItem.class, "Furniture" ); public RepositoryFactory(Firestore firestore){ getCollectionReference = collectionPath -> firestore.collection(collectionPath); } private RepositoryFactory(DocumentReference document){ getCollectionReference = collectionPath -> document.collection(collectionPath); } public RepositoryFactory FromDocument(Class> classType, String documentId){ String collectionName = collectionMap.get(classType); DocumentReference documentReference = getCollectionReference.apply(collectionName).document(documentId); return new RepositoryFactory(documentReference); } public IRepository GetRepository(Class classType){ String collectionName = collectionMap.get(classType); return new FirestoreRepository (getCollectionReference.apply(collectionName), classType); } }
Подводить итоги
Вот как я реализовал шаблон репозитория для работы с вложенными коллекциями Firestore. Для этого примера я решил использовать Java, но его должно быть легко перевести на другие языки.
Открытые точки
В этом посте я упустил из виду некоторые аспекты, которые можно было бы рассмотреть в отдельных темах:
- Как бороться с Google Будущее Api и Java Завершаемый в будущем .
- Как работать с запросами, специфичными для модели.
Технологии
- Ява
- Огненный камень
- Вложенные коллекции
- Шаблон хранилища
Дайте мне знать, как вы работаете с вложенными коллекциями!
Оригинал: “https://dev.to/giodiblasi/a-repository-pattern-implementation-for-firestore-subcollections-463”