Рубрики
Без рубрики

Реализация шаблона репозитория для Вложенные коллекции Firestore

Если вы уже используете Google Cloud Firestore, вы, вероятно, знаете его модель данных и заметили этот подраздел… С пометкой firestore, java.

Если вы уже используете 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  FirestoreRepository implements 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, Class classType){
        this.collection = collection;
        this.classType = classType;
    }

Таким образом, мы устранили предположение о том, что каждая коллекция находится на корневом уровне.

Фабрика-хранилище

На этом этапе позвольте мне представить RepositoryFactory чья ответственность заключается:

  • цепочка Ссылки на коллекции и Ссылки на документы
  • создайте экземпляр репозитория для определенной коллекции (или вложенной коллекции).

Например, если мы хотим получить хранилище для “Предметов мебели” документа “ванная комната” относительно “Мой дом”, мы ожидаем получить что-то вроде:

  IRepository furnitureRepo = repoFactory
        .FromDocument(House.class, "My-House")
        .FromDocument(Room.class, "bathroom")
        .GetRepository(FurnitureItem.class);

    furnitureRepo.Add(new FurnitureItem("chair"));

В противном случае, если мы хотим выполнить запрос через коллекцию House , мы должны иметь:

  IRepository houseRepo = 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 потребности:

  1. Карта для связи типа класса модели с именем коллекции
  2. Указатель для создания репозитория: если репозиторий находится на корневом уровне, то это должен быть экземпляр firestore, Ссылка на документ в противном случае.
  3. Способ перемещения указателя на следующий Ссылка на документ возвращает новый экземпляр RepositoryFactory .
  4. Способ получения экземпляра Репозитория для указанной коллекции (или Вложенной коллекции )

Карта Коллекции Моделей

Чтобы связать каждый тип класса модели с именем соответствующей коллекции, мы используем такую карту:

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 Function getCollectionReference;
    ....

    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);
    }

Создайте репозиторий для данной модели

На этом этапе нам нужен метод, который, учитывая тип класса модели, возвращает экземпляр Репозитория<> :

 public  IRepository 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”