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.


Contents

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 Jar

Guide

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).
It is preferable to avoid constructors that include the id attribute as a parameter because the ID field will be initialized once the object is written on Firestore. The ID attribute's value indicates an object that also exists on Firestore. The value null for the ID (e.g. before being written or after being deleted) indicates that this object does not exist on Firestore.

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);
The register() method will throw a runtime exception when the conditions stated above are not met by the class being registered.

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);
If the create() method returns a null ID, this is an indication that the object has not been successfully written to Firestore.

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");
Creating an item with the same ID as another existing item of the same class will replace the existing item. Using specific IDs is not recommended.

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");
});
Only changed attributes will be updated on Firestore.
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");
});
Google will charge your Firestore instance by the amount of reads and writes occurring on your database. This is why the list() method forces you to limit the amount of objects retrieved.
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");
});
Google will charge your project $0.30-$0.60 per million reads depending on location. Click here to find out more about Firebase pricing.

For properly listing large numbers of objects, consider using Pagination.
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();
Firestore will automatically create indexes for simple queries. However, some compound queries will require you to define compound Firestore indexes. Click here to learn more about composite Firebase indexes.
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 ...
com.raylabz.firestorm.Firestorm will automatically update your object with the new data once an update is received.
Listeners will listen to updates on items while the application is running, but will not block the execution of the program. If the application is stopped, the listener will also be terminated.

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.");
    }
});
You can read more about Firestore transactions and batch writes here.

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

View the 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.