DEV Community

Giovanni Di Blasi
Giovanni Di Blasi

Posted on

A Repository Pattern Implementation for Firestore Subcollections

If you already use Google Cloud Firestore, you probably know its data model and you noticed that Subcollections are one of the most interesting features.
I met them for the first time while working on my personal pet-project (a project where I can experiment with new technologies and methodologies).

So, I wondered if there was a way to adapt the Repository Pattern to deal with Firestore Subcollections.
The main aspect to address was that in the Repository Pattern each repository is usually related to a specific model, in other words, it is related to a type. But reading from Google documentation page:

A Subcollection is a collection associated with a specific document.

This means that, for a subcollection, a repository is not related to a type but to a specific Document.

Let's see this experiment results... πŸ”¬

Scenario

To get the point imagine this practical example:
We have to deal with "Houses" and each of these have some "Rooms" (a bathroom, a kitchen, ...). In each room there is the "Furniture" (a bed, two chairs and so on).
To model this scenario we can imagine to implement these entities:

  • Furniture Item
  • Room
  • House

and they will be related in this way:

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

The Repository

For the sake of simplicity let's focus on these two methods

  • "Add" adds a document to the collection
  • "GetById" returns a document by its Id

Speaking in terms of interface we have:

public interface IRepository<T>{
    Either<String,String> Add(T data);
    Either<String, T> GetById(String id);
}

Ignoring for a moment Subcollections, an implementing class could be:

public  class  FirestoreRepository<T> implements IRepository<T>{

    private CollectionReference collection;
    private Class<T> classType;

    public FirestoreRepository(Firestore firestore, String collectionPath, Class<T> classType){
        this.collection = firestore.collection(collectionPath);
        this.classType = classType;
    }

    public Either<String,String> Save(T data){ 
        try{
            ApiFuture<DocumentSnapshot> 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<String, T> GetById(String id) {
        try{
            ApiFuture<DocumentSnapshot> 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());
        }
    }
}

Note:
The GoogleApi methods to retrieve a DocumentSnasphot are async, they indeed returns an ApiFuture (a GoogleAPI implementation of java Future).

For the sake of simplicity, I chose to avoid the async approach blocking the execution until the future is complete.
Yes, I know, It's not a good idea to do in real projects πŸ€“


The above implementation assumes that all the collection are at root level, in the constructor the collection reference is indeed created from the firestore object:

this.collection = firestore.collection(collectionPath);

This assumption is not correct when we use the firestore Subcollections. A Subcollection is related to a specific document, so we need
a DocumentReference to get one of its collections.
For example:

CollectionReference furnitureCollection = firestore
        .collection("Houses")
        .document("My-House")
        .collection("Rooms")
        .document("bathroom")
        .collection("FurnitureItems");

We notice that we need to chain CollectionReferences and DocumentReference to access to the right Subcollection.

So, as first, we need to modify the constructor of our FirestoreRepository to accept a CollectionReference.

public FirestoreRepository(CollectionReference collection, Class<T> classType){
        this.collection = collection;
        this.classType = classType;
    }

In this way we have removed the assumption that each collection is at root level.

The Repository Factory

At this point let me introduce the RepositoryFactory whose responsibility is:

  • chain CollectionReferences and DocuementReferences
  • create an IRepository instance for a specific Collection (or Subcollection).

For example, if we want to get the Repository for the "FurnitureItems"
of the document "bathroom" relative to the "My-House", we expect to have something like:

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

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

Otherwise, if we want to query through the House collection we should have:

  IRepository<House> houseRepo = repoFactory.GetRepository(FurnitureItem.class);
  houseRepo.Add(new House());

We notice that the method FromDocument should return a new instance of RepositoryFactory. This allows us to keep a RepositoryFactory reference to access all its Subcollections.
For example consider that a "House", in addition to the "Rooms" collection, has a "Tenants" collection. In this scenario we can write:

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

Design the RepositoryFactory πŸ“ ✏️

Bearing in mind everything, our RepositoryFactory needs:

  1. A map to link the Model classType with a collection name
  2. A pointer to create a repository: if the repo is at root level then it should be a firestore instance, a DocumentReference otherwise.
  3. A method to move the pointer to the next DocumentReference returning a new RepositoryFactory instance.
  4. A method to get the IRepository instance for a specified collection (or Subcollection)

The Model-Collection Map

To link each model class type with the relative collection name we use a map like this:

private final static Map<Class<?>, String> collectionMap = Map.of(
        House.class, "Houses",
        Room.class, "Rooms",
        FurnitureItem.class, "Furniture"
    );   

The Pointer

Keep in mind that we have two kinds of pointer:

  • The Firestore instance if the collection is at the root level
  • A DocumentReference if the collection is relative to a specific Document (a.k.a Subcollection)

We could keep a reference to Firestore and one to a DocumentReference, then we should have some logic to choose what of them to use.

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

But when move the pointer from the root level to a document level, the firestore reference is no more useful.
Is there a way to keep only the pointer we need?

πŸ’‘ Functional approach to the rescue!

We could indeed keep only a reference to a function that, given a collection name, retrieves a CollectionReference:

public class RepositoryFactory{
    ....
    private Function<String, CollectionReference> getCollectionReference;
    ....

    public RepositoryFactory(Firestore firestore){
        getCollectionReference = collectionPath -> firestore.collection(collectionPath);
    }

    private RepositoryFactory(DocumentReference document){
        getCollectionReference = collectionPath -> document.collection(collectionPath);
    }
    ....
}

In this way, each instance of RepositoryFactory keeps only the reference to the right pointer.

Move the pointer to a specific Document

To move through DocumentsReference(s) we don't want to modify the existing RepositoryFactory instance but we have to create a new one with the new DocumentReference as pointer:


 public  RepositoryFactory FromDocument(Class<?> classType, String documentId){
        String collectionName = collectionMap.get(classType);
        DocumentReference documentReference = getCollectionReference.apply(collectionName).document(documentId);
        return new RepositoryFactory(documentReference);
    }

Create a Repository for a given Model

At this point we need a method that, given a Model classType, returns a IRepository<> instance:

 public <T> IRepository<T> GetRepository(Class<T> classType){
        String collectionName = collectionMap.get(classType);
        return new FirestoreRepository<T>(getCollectionReference.apply(collectionName), classType);
    }

Pick up the pieces

Finally, our Repository Pattern implementation should be something like this:


//IRepository.java
public interface IRepository<T>{
    Either<String,String> Add(T data);
    Either<String, T> GetById(String id);
}

//FirestoreRepository.java
public  class  FirestoreRepository<T> implements IRepository<T>{

    private CollectionReference collection;
    private Class<T> classType;

    public FirestoreRepository(CollectionReference collection, Class<T> classType){
        this.collection = collection;
        this.classType = classType;
    }

    public Either<String,String> 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<String, T> 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<String, CollectionReference> getCollectionReference;

    private final static Map<Class<?>, 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 <T> IRepository<T> GetRepository(Class<T> classType){
        String collectionName = collectionMap.get(classType);
        return new FirestoreRepository<T>(getCollectionReference.apply(collectionName), classType);
    }
}

To sum up

This is how I implemented the repository pattern to deal with Firestore subcollections.
For this example I chose to use Java, but it should be easy to move to other languages.

Open points

In this post I have overlooked some aspects that could be addressed in separate topics:

  • How to deal with Google ApiFuture and Java CompletableFuture.
  • How to deal with queries specific for a model.

Technologies

  • Java
  • Firestore
  • Subcollections
  • Repository Pattern

Let me know your way to work with subcollections!

Top comments (1)

Collapse
 
francomelandri profile image
Franco Melandri

Well done Giovanni!