com.raylabz.firestorm.Firestorm
An object-oriented data access API for Google's Firestore
com.raylabz.firestorm.Firestorm is an object-oriented data access API for Google's Firestore. It enables developers to rapidly develop applications that utilize Firestore's capabilities by interacting with it in an object-oriented way. com.raylabz.firestorm.Firestorm uses standardized functions and complements the full flexibility of Google's Firestore API, with no performance penalty. It organizes classes as Firestore collections and objects of these classes as documents in these collections. Most importantly, it aims to reduce code, improve its readability and support rapid application development.
com.raylabz.firestorm.Firestorm is implemented for Java server-side and Android apps. A Dart-based version of the library as a Flutter plugin is being considered.
Not convinced?
Check out the code
comparison!
Contents
- Download and import
- Guide
- Documentation
- Source code
- License
- Bug reporting
warning
com.raylabz.firestorm.Firestorm is a library intended for server-side use. It uses the Firebase Admin SDK, requires service account credentials that must be safely stored, and gives access to the full database. Therefore, it is not intended for use in unsecure environments, such as client-side applications.
android
If you are developing an Android app, you can use com.raylabz.firestorm.Firestorm for Android.
Download and import
You can easily import com.raylabz.firestorm.Firestorm in your project using Maven or Gradle:
Maven:
<dependency> <groupId>com.raylabz</groupId> <artifactId>firestorm</artifactId> <version>1.4.0</version> </dependency>
Gradle:
implementation 'com.raylabz:firestorm:1.4.0'
Alternatively, you can download com.raylabz.firestorm.Firestorm as a .jar file:
Download JarGuide
Initialization
To use com.raylabz.firestorm.Firestorm, set up and initialize the Firebase Admin SDK as described here.
You can initialize com.raylabz.firestorm.Firestorm by calling com.raylabz.firestorm.Firestorm.init() after initializing your FirebaseApp. You do not need to initialize a Firestore object as this is automatically done by this method.
//Initialize Firebase (using a Service Account configuration file): try { FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json"); FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setDatabaseUrl("https://<YOUR_PROJECT_ID>.firebaseio.com/") .build(); FirebaseApp.initializeApp(options); } catch (IOException e) { e.printStackTrace(); }//Initialize com.raylabz.firestorm.Firestorm: com.raylabz.firestorm.Firestorm.init();
Initialization on the GCP
Alternatively, if your app is hosted on a Google Cloud Platform server (e.g. App Engine or Compute Engine), you can initialize Firebase using the default service account:
//Initialize Firebase (using application default credentials): try { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setDatabaseUrl("https://<YOUR_PROJECT_ID>.firebaseio.com/") .build(); FirebaseApp.initializeApp(options); } catch (IOException e) { e.printStackTrace(); }//Initialize com.raylabz.firestorm.Firestorm: com.raylabz.firestorm.Firestorm.init();
Google App Engine initialization
Click here for a guide on setting up com.raylabz.firestorm.Firestorm with Google App Engine.Custom classes
com.raylabz.firestorm.Firestorm allows you to create your own custom classes, which it then can interact with in Firestore. Your classes must:
- Be annotated with the @FirestormObject annotation.
- Have an attribute called id of type String, or extend a class that has this field.
- Have an empty (no-parameter) constructor (access modifier does not matter).
The following code shows an example on how to create a class:
@FirestormObject public class Person { private String id; private String firstName; private String lastName; private int age; public Person(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } private Person() { } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getId() { return id; } public void setId(String id) { this.id = id; } }
Excluding attributes
You can exclude certain class attributes from being stored in the Firestore by annotating their getter functions with the @Exclude annotation:
@FirestormObject public class Person { private String id; private String firstName; private String lastName; private int age; private int ignoredField; public Person(String firstName, String lastName, int age, int ignoredField) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.ignoredField = ignoredField; } private Person() { } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getId() { return id; } public void setId(String id) { this.id = id; } @Exclude public int getIgnoredField() { return ignoredField; } public void setIgnoredField(int ignoredField) { this.ignoredField = ignoredField; } }
Registering classes
Before using com.raylabz.firestorm.Firestorm, you have to register your classes. This process is necessary as com.raylabz.firestorm.Firestorm will check that your classes meet the requirements.
- Each class must be annotated with @FirestormObject.
- Each class must have an attribute called id, of type String, or extend a class that has this field.
- Each class must have an empty (no-parameter) constructor, regardless of access modifier.
You can register your class after initializing com.raylabz.firestorm.Firestorm, by using the register() method, providing your class as a parameter:
com.raylabz.firestorm.Firestorm.register(Person.class);
When using in Google's App Engine, you can register your classes by calling the register() method within a context listener:
public class FirestormContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { //Initialize Firebase: ... //Initialize com.raylabz.firestorm.Firestorm: ... //Register classes: com.raylabz.firestorm.Firestorm.register(Person.class); } }
Basic operations
Create
You can create objects in Firestore by using com.raylabz.firestorm.Firestorm.create() and providing an instance of your custom class:
com.raylabz.firestorm.Firestorm.create(person);
Creating an object will set its id attribute which is null by default. You can retrieve this attribute and save it for reference if needed:
String personID = com.raylabz.firestorm.Firestorm.create(person);
You can create an object using a specific ID by passing the ID as a second parameter to the create() method:
com.raylabz.firestorm.Firestorm.create(person, "abcde");
You can also attach an OnFailureListener to this call, which will allow you to define what happens when this operation fails:
com.raylabz.firestorm.Firestorm.create(person, new OnFailureListener() { @Override public void onFailure(Exception e) { System.out.println("FAILED"); } });
Or briefly, using lambda expressions:
com.raylabz.firestorm.Firestorm.create(person, error -> { System.out.println("FAILED"); });
Get
You can retrieve objects from the Firestore by using Firestore.get() and providing the class of the object and its document ID:
Person person = com.raylabz.firestorm.Firestorm.get(Person.class, documentID);
The get() method will return null when the object does not exist or com.raylabz.firestorm.Firestorm fails to fetch the object from Firestore.
Using an OnFailureListener:
Person person = com.raylabz.firestorm.Firestorm.get(Person.class, documentID, new OnFailureListener() { @Override public void onFailure(Exception e) { System.out.println("FAILED TO GET OBJECT"); } });
Get many
You can retrieve multiple objects of the same class from Firestore by using Firestore.getMany(), providing the class of the objects and their document IDs as a list:
List<Person> items = com.raylabz.firestorm.Firestorm.getMany(Person.class, documentIDs); //where documentIDs is a list of Strings
Using an OnFailureListener:
List<Person> items = com.raylabz.firestorm.Firestorm.getMany(Person.class, documentIDs, new OnFailureListener() { @Override public void onFailure(Exception e) { System.out.println("FAILED TO GET MANY OBJECTS"); } });
You may also use a varags array of Strings:
List<Person> items = com.raylabz.firestorm.Firestorm.getMany(Person.class, "id1", "id2", "id3");
Exists
You can check if an object document exists on Firestore by providing its class and a document ID:
if (com.raylabz.firestorm.Firestorm.exists(Person.class, documentID)) { //TODO - Document exists... } else { //TODO - Document does not exist... }
Update
You can update objects by using com.raylabz.firestorm.Firestorm.update() and providing your object:
com.raylabz.firestorm.Firestorm.update(person);
Similarly, using an OnFailureListener:
com.raylabz.firestorm.Firestorm.update(person, error -> { System.out.println("FAILED"); });
List
com.raylabz.firestorm.Firestorm supports fetching objects of a given type using the com.raylabz.firestorm.Firestorm.list() method for up to a certain number of objects. The list() method is meant for use with classes/types that do not have many objects and which do not normally grow a lot. You can list objects of a type by providing the class and maximum number of objects to retrieve as parameters:
ArrayList<Person> peopleList = com.raylabz.firestorm.Firestorm.list(Person.class, 100);
The above code will retrieve at maximum 100 objects of type Person.
Similarly, using an OnFailureListener:
ArrayList<Person> peopleList = com.raylabz.firestorm.Firestorm.list(Person.class, 100, error -> { System.out.println("FAILED"); });
List all
If you still need to fetch all objects of a given type, you can use the com.raylabz.firestorm.Firestorm.listAll() method, which allows you to do that without any limitations:
ArrayList<Person> peopleList = com.raylabz.firestorm.Firestorm.listAll(Person.class);
ArrayList<Person> peopleList = com.raylabz.firestorm.Firestorm.listAll(Person.class, error -> { System.out.println("FAILED"); });
Delete
You can delete an object by using the com.raylabz.firestorm.Firestorm.delete() method. Deleting an object removes its id attribute and sets it back to null.
com.raylabz.firestorm.Firestorm.delete(person);
com.raylabz.firestorm.Firestorm.delete(person, error -> { System.out.println("FAILED"); });
Obtaining document references
You can obtain a reference to an object's Firestore document by using the getObjectReference() on an object. This returns a DocumentReference which can be used with the regular Firestore API.
DocumentReference objectReference = person.getObjectReference();
Alternatively, you can provide an object type and a document ID:
DocumentReference objectReference = com.raylabz.firestorm.Firestorm.getObjectReference(Person.class, documentID);
Obtaining collection references
You can also obtain a reference a type's collection in Firestore by using com.raylabz.firestorm.Firestorm.getCollectionReference(). This returns a CollectionReference which can be used with the regular Firestore API.
CollectionReference collectionReference = com.raylabz.firestorm.Firestorm.getCollectionReference(Person.class);
Filtering
com.raylabz.firestorm.Firestorm allows you to easily filter results using the filter() method. This method expects a type as a parameter and will return a FirestormFilterable object:
final FirestormFilterable<Person> filter = com.raylabz.firestorm.Firestorm.filter(Person.class);
Items can be filtered using several methods. It is preferable to chain these methods in order to make the code easier to read:
final FirestormFilterable<Person> filter = com.raylabz.firestorm.Firestorm.filter(Person.class) .whereEqualTo("firstName", "John") .whereGreaterThan("age", 10) .orderBy("age") .limit(5);
You can get the results of a filter by using the fetch() method. This returns a QueryResult object:
final QueryResult<Person> result = com.raylabz.firestorm.Firestorm.filter(Person.class) .whereEqualTo("firstName", "John") .whereGreaterThan("age", 10) .orderBy("age") .limit(5) .fetch();
Finally, you can get the items retrieved by your query using the getItems() method, which returns an ArrayList of items:
final ArrayList<Person> items = result.getItems();
You can also get a) the query snapshot, b) the last returned item's ID, and c) whether or not the result has returned any items using:
//Get query snapshot: final QueryDocumentSnapshot snapshot = result.getSnapshot(); //Get the last item's ID: final String lastItemID = result.getLastDocumentID(); //Check if the result has returned and items: final boolean hasItems = result.hasItems();
Filters reference
The following is a table of filters, ordering, limits etc. which can be applied to a query via the filter() method:
Method | Use | Params |
---|---|---|
whereEqualTo | Returns objects that have a field with a value equal to the given value. | Field name, value Field path, value |
whereLessThan | Returns objects that have a field with a value less than the given value. | Field name, value Field path, value |
whereLessThanOrEqualTo | Returns objects that have a field with a value less than or equal to the given value. | Field name, value Field path, value |
whereGreaterThan | Returns objects that have a field with a value greater than the given value. | Field name, value Field path, value |
whereGreaterThanOrEqualTo | Returns objects that have a field with a value greater than or equal to the given value. | Field name, value Field path, value |
whereArrayContains | Returns objects that have a field (array) containing the given value. | Field name, value Field path, value |
whereArrayContainsAny | Returns objects that have a field (array) containing any of a given list of values. | Field name, list of values Field path, list of values |
whereIn | Returns objects that have a field containing any of a given list of values. | Field name, list of values Field path, list of values |
whereNotIn | Returns objects of which a field does not contain any of a given list of values. | Field name, list of values Field path, list of values |
orderBy | Returns objects ordered by a specific field. | Field name Field path Field name, Direction of order Field path, Direction of order |
limit | Limits the number of results returned by the query. | Limit (number) |
offset | Offsets the query's start position by a given amount. | Offset(number) |
startAt | Starts the query at a specific document. | DocumentSnapshot List of field values |
select | Selects only specific fields to return from an object. | List of fields List of field paths |
startAfter | Starts the query after a specified document. | DocumentSnapshot List of field values |
endBefore | Ends the query before a specified document. | DocumentSnapshot List of field values |
endAt | Ends the query at a specified document. | DocumentSnapshot List of field values |
stream | Streams the query to an observer. | ApiStreamObserver |
get | Retrieves a snapshot of the query. | - |
addSnapshotListener | Adds a snapshot listener to the query. | EventListener Executor and EventListener |
hashCode | Retrieves the hash code of the query. | - |
fetch | Fetches the results of the query directly. | - |
Real-time updates
Attaching listeners to objects/documents
com.raylabz.firestorm.Firestorm allows you to use Firestore's real-time update features by attaching listeners to objects using the attachListener() method and then implementing an OnObjectUpdateListener. Note that to instantiate a listener you need to provide an object to listen to (in this case 'person'):
com.raylabz.firestorm.Firestorm.attachListener(new OnObjectUpdateListener(person) { @Override public void onSuccess() { System.out.println(person); } @Override public void onFailure(String failureMessage) { System.err.println("Failed to update person"); } });
The attachListener() method returns a ListenerRegistration object. You can save this object in a variable for future use (such as detaching the listener):
final ListenerRegistration listenerRegistration = com.raylabz.firestorm.Firestorm.attachListener ...
Alternatively, you can attach a listener to a Firestore document reference by specifying a class and a document ID:
com.raylabz.firestorm.Firestorm.attachListener(new OnReferenceUpdateListener(Person.class, documentID) { @Override public void onSuccess(Object object) { System.out.println(object); } @Override public void onFailure(String failureMessage) { System.err.println("Failed to update person"); } });
Attaching listeners to classes/collections
Real-time listeners can also be attached to entire classes/collections. These are useful in cases where you need to perform a task when objects of a certain class are modified, removed, or created. To create a class/collection update listener, you can use the attachListener() method, but instead provide a OnCollectionUpdateListener:
com.raylabz.firestorm.Firestorm.attachListener(new OnCollectionUpdateListener<Person>(Person.class) { @Override public void onSuccess(List<ObjectChange<Person>> objectChanges) { for (ObjectChange<Person> objectChange : objectChanges) { System.out.println(objectChange.getObject()); } } @Override public void onFailure(String failureMessage) { System.err.println(failureMessage); } });
Attaching listeners to filtered items
You can also attach a listener to a specific selection of items, by first creating a filter using the
filter()
method, and then providing the filter created to a FilterableListener
through the attachListener()
method:
//Create your filter first, using the filter() method: FirestormFilterable<Person> filterable = com.raylabz.firestorm.Firestorm.filter(Person.class) .whereGreaterThan("age", 12); //Then attach a FilterableListener that uses the filter created above: com.raylabz.firestorm.Firestorm.attachListener(new FilterableListener<Person>(filterable) { @Override public void onSuccess(List<ObjectChange<Person>> objectChanges) { for (ObjectChange<Person> objectChange : objectChanges) { System.out.println(objectChange.getObject().toString()); } } @Override public void onFailure(String failureMessage) { System.err.println(failureMessage); } });
In the example above, com.raylabz.firestorm.Firestorm will listen to changes to objects of the type Person
, for which the field
is >12. If an object's age value is altered to not match this filter (i.e. age=11), com.raylabz.firestorm.Firestorm will ignore updates
to this object. Conversely, if the object's age value is then changed to meet the criteria (i.e. age=13),
com.raylabz.firestorm.Firestorm will resume listening to updates on this object.
Detaching listeners
You can detach a listener from an object by passing a saved ListenerRegistration instance to the detachListener() method:
person.detachListener(listenerRegistration);
You can also detach a listener from a specific object or class:
person.detachListener(person);
person.detachListener(Person.class);
Checking if object or class has an attached listener
The com.raylabz.firestorm.Firestorm.hasListener() method allows you to check if an object or class has an attached listener:
com.raylabz.firestorm.Firestorm.hasListener(person);
or
com.raylabz.firestorm.Firestorm.hasListener(Person.class);
Get last attached object listener
The com.raylabz.firestorm.Firestorm.getListener() method allows you to retrieve an object's last attached listener:
ListenerRegistration listener = com.raylabz.firestorm.Firestorm.getListener(person);
Transactions
com.raylabz.firestorm.Firestorm allows you to quickly define and run a Firestore transaction using the runTransaction() method. To do so, you first need to declare a new FirestormTransaction:
FirestormTransaction transaction = new FirestormTransaction() { @Override public void execute() { create(person1); get(person2); update(person2); delete(person3); list(Person.class); } @Override public void onSuccess() { System.out.println("Transaction successful!"); } @Override public void onFailure(Exception e) { System.out.println("Transaction failed."); } };
You can then run your transaction using the runTransaction() method:
com.raylabz.firestorm.Firestorm.runTransaction(transaction);
Alternatively, you can directly run a nameless transaction:
com.raylabz.firestorm.Firestorm.runTransaction(new FirestormTransaction() { @Override public void execute() { create(person1); get(person2); update(person2); delete(person3); list(Person.class); } @Override public void onSuccess() { System.out.println("Transaction successful!"); } @Override public void onFailure(Exception e) { System.out.println("Transaction failed."); } });
Batch writes
com.raylabz.firestorm.Firestorm also allows you to quickly define and run batch write operations on Firestore, using the runBatch() You can define a new batch by creating a new FirestormBatch object:
FirestormBatch batch = new FirestormBatch() { @Override public void execute() { create(person); update(person2); delete(person3); } @Override public void onSuccess() { System.out.println("Batch operation successful!"); } @Override public void onFailure(Exception e) { System.out.println("Batch operation failed."); } };
Run a batch using the runBatch() method:
com.raylabz.firestorm.Firestorm.runBatch(batch);
Alternatively, using a nameless object:
com.raylabz.firestorm.Firestorm.runBatch(new FirestormBatch() { @Override public void execute() { create(person); update(person2); delete(person3); } @Override public void onSuccess() { System.out.println("Batch operation successful!"); } @Override public void onFailure(Exception e) { System.out.println("Batch operation failed."); } });
Pagination
com.raylabz.firestorm.Firestorm supports easy pagination of large numbers of objects of a specified type using the Paginator class. The paginator's next() method receives the type of objects to paginate and the last listed document's ID. In the first call, the last listed document's ID should be passed as null:
String lastDocumentID = null; Paginator<Person> paginator = Paginator.next(Person.class, lastDocumentID);
You can also pass a limit for the number of results per page in the next() method. By default, the number of results per page is set to 10.
Paginator<Person> paginator = Paginator.next(Person.class, lastDocumentID, 10);
Then, you can retrieve the results of the pagination using the fetch() method:
QueryResult<Person> result = paginator.fetch();
From the results object, you can use the getItems() method to retrieve the items:
final ArrayList<Person> items = result.getItems();
You can also use the getLastDocumentID() method to get the ID of the last document in the current results. You will need use this ID later to get the next page of results.
lastDocumentID = result.getLastDocumentID();
You can then pass the last document's ID to the paginator's next() method again to retrieve the next page of results, and so on:
paginator = Paginator.next(Person.class, lastDocumentID);
Furthermore, you can check if a result contains items without getting its items array using the hasItems() method:
boolean hasItems = result.hasItems();
Full pagination example:
This example continues looping and fetching new results until there are no items left to fetch:
QueryResult<Person> result; String lastDocumentID = null; do { result = Paginator.next(Person.class, lastDocumentID).fetch(); System.out.println("Fetched items: " + result.getItems().size()); for (Person p : result.getItems()) { System.out.println("-> " + p.getFirstName()); } lastDocumentID = result.getLastDocumentID(); new Scanner(System.in).nextLine(); } while (result.hasItems());
Pagination with filtering
You also can use filters and ordering for paginated results:
paginator = Paginator.next(Person.class, lastDocumentID, 10) .whereEqualTo("age", 40);
paginator = Paginator.next(Person.class, lastDocumentID, 50) .whereGreaterThan("age", 5);
paginator = Paginator.next(Person.class, lastDocumentID) .whereEqualTo("firstName", "John") .whereGreaterThan("age", 5) .orderBy("age");
Documentation
License
com.raylabz.firestorm.Firestorm is released under the MIT license.
Source code
You can find the source code at the project's repository here.
Bug reporting
Please report bugs here.