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:
- A map to link the Model classType with a collection name
- A pointer to create a repository: if the repo is at root level then it should be a firestore instance, a DocumentReference otherwise.
- A method to move the pointer to the next DocumentReference returning a new RepositoryFactory instance.
- 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)
Well done Giovanni!