jFirebase - Entity Framework for Firebase/Firestore

5 minute read

I decided to tryout firebase for my personal projects & learning, having come from traditional SQL background, I am well versed with JPA/Hibernate when dealing with databases. But when I started firebase, I found it completely different but also having some similarities. After spending some time understanding its feature I started looking a framework similar to JPA for firebase. I found none, so I decided to write one for myself.

Github Source - jFirebase

Initialise

Add Maven dependency:

        <dependency>
            <groupId>in.kuros</groupId>
            <artifactId>jFirebase</artifactId>
            <version>0.1</version> <!-- Use the latest version-->
        </dependency>

Use the PersistenceServiceFactory to generate instance of PersistenceService, we need pass firestore instance and provide a list of packages where your entities reside.

// initialise with firestore & base package
final PersistenceService persistenceService = PersistenceServiceFactory.create(firestore, "in.kuros.jfirebase.demo.entity");

Now that we have initialized our persistence service we will start creating our entities to work with.

Entity

We will use a simple pojo class and convert it into an jFirebase Entity by providing @Entity, you will also need to provide @Id field.

Currently only String & Enum are supported as Id field.


package in.kuros.jfirebase.demo.entity;

import in.kuros.jfirebase.entity.CreateTime;
import in.kuros.jfirebase.entity.Entity;
import in.kuros.jfirebase.entity.Id;
import in.kuros.jfirebase.entity.UpdateTime;
import lombok.Data;

import java.util.Date;

@Data
@Entity("person")
public class Person {

    @Id
    private String personId;
    private String name;

    @CreateTime
    private Date created;
    @UpdateTime
    private Date lastModified;
}

We can also mark Date field for @CreateTime & @UpdateTime to record creation time & update time implicitly.

Metadata

We will also need to create metadata class to record field description for the corresponding class.

package in.kuros.jfirebase.demo.entity;

import in.kuros.jfirebase.metadata.Attribute;
import in.kuros.jfirebase.metadata.Metadata;

import java.util.Date;

@Metadata(Person.class)
public class Person_ {

    public static volatile Attribute<Person, String> personId;
    public static volatile Attribute<Person, String> name;
    public static volatile Attribute<Person, Date> created;
    public static volatile Attribute<Person, Date> lastModified;
}

Annotate the class with @Metadata and create Attribute field for all the corresponding fields on main class.

Our setup is almost done, lets create our first object using persistenceService.

Creating object

To create a new entry in database use create method. It will auto generate Id & createTime will be updated.

    public void createPersonExample() {
        final Person p = new Person();
        p.setName("Rohit");

        persistenceService.create(p);

        System.out.println(p.getPersonId());
    }

jfirebase-create

Creating object with id

You can create entities with custom ids.

    public void createPersonWithCustomIdExample() {
        final Person p = new Person();
        p.setPersonId("1");
        p.setName("Rohit");

        persistenceService.create(p);

        System.out.println(p.getPersonId());
    }

jfirebase-create

Note: Create method will throw exception if entity with id already exists.

Update/Silent create

Use set method to create/update entity silently, ie. if entity exists, it will be updated else a new entry will be created.

    public void updatePersonExample() {
        final Person p = new Person();
        p.setPersonId("1"); // optional if you want to create a new entry
        p.setName("Rohit");

        persistenceService.set(p);

        System.out.println(p.getPersonId());
    }

The set(entity) would update the complete entity.

Updating field

Let’s say I want to update age of person. We need to provide entity with id and age field populated (in this case personId is a requiredField).

    public void updateField() {
        final Person p = new Person();
        p.setPersonId("1"); // optional if you want to create a new entry
        p.setAge(20);

        persistenceService.set(p, Person_.age);
    }

If id field is provided, given field will be updated else a new entry is created.

jfirebase-set

Updating multiple fields

We can update multiple fields, Use AttributeValue

    public void updateMultipleFields() {
        persistenceService.set(AttributeValue.with(Person_.personId, "1")
                .with(Person_.name, "I changed my name")
                .with(Person_.age, 25)
                .build());
    }

jfirebase-set

Working with Sub Collection

Let’s say you want to map sub collection, you need to create a pojo with parent id reference.

package in.kuros.jfirebase.demo.entity;

import in.kuros.jfirebase.entity.Entity;
import in.kuros.jfirebase.entity.Id;
import in.kuros.jfirebase.entity.IdReference;
import in.kuros.jfirebase.entity.Parent;
import in.kuros.jfirebase.entity.UpdateTime;

import java.util.Date;
import java.util.Map;

@Entity("employee")
public class Employee {

    @Id
    private String employeeId;

    @Parent
    @IdReference(Person.class)
    private String personId;

    private Date joiningDate;
    private Integer salary;
    private Map<String, String> phoneNumbers;

    @UpdateTime
    private Date modifiedDate;
}

Here we are using @Parent with @IdReference to map parent information.

Also note, to save phone numbers we are using a map. Its corresponding Metadata class will be:

package in.kuros.jfirebase.demo.entity;

import in.kuros.jfirebase.entity.Entity;
import in.kuros.jfirebase.metadata.Attribute;
import in.kuros.jfirebase.metadata.MapAttribute;
import in.kuros.jfirebase.metadata.Metadata;

import java.util.Date;

@Metadata(Employee.class)
public class Employee_ {

    public static volatile Attribute<Employee, String> employeeId;
    public static volatile Attribute<Employee, String> personId;
    public static volatile Attribute<Employee, Date> joiningDate;
    public static volatile Attribute<Employee, Integer> salary;
    public static volatile MapAttribute<Employee, String, String> phoneNumbers;
    public static volatile Attribute<Employee, Date> modifiedDate;

}

Now to create a Employee entry within Person collection, simply create Employee object with person reference. Note that for phoneNumbers we are using MapAttribute.

    public void createSubCollection() {
        final Employee employee = new Employee();
        employee.setEmployeeId("123"); // Optional if you want custom id
        employee.setPersonId("1");
        employee.setJoiningDate(new Date());
        employee.setSalary(5000);

        final Map<String, String> phoneNumbers = new HashMap<>();
        phoneNumbers.put("office", "123-345-567");
        phoneNumbers.put("home", "456-789-456");
        employee.setPhoneNumbers(phoneNumbers);

        persistenceService.create(employee);
    }

jfirebase-sub-collection

Update Map values

Let’s say we want to update specific value in a map (home phone number).

    public void updateMapUsingKeyValue() {
        persistenceService.set(AttributeValue
                .with(Employee_.employeeId, "123") // Required field
                .with(Employee_.personId, "1") // Required field
                .with(Employee_.phoneNumbers, "home", "111-111-111")
                .build());
    }

jfirebase-map-key

or we can completely replace the map - update all the phone numbers

    public void updateCompleteMapValues() {
        final Map<String, String> phoneNumbers = new HashMap<>();
        phoneNumbers.put("office", "123-345-XXX");
        phoneNumbers.put("home", "456-789-XXX");

        persistenceService.set(AttributeValue
                .with(Employee_.employeeId, "123") // Required field
                .with(Employee_.personId, "1") // Required field
                .with(Employee_.phoneNumbers, phoneNumbers)
                .build());
    }

jfirebase-map

Removing Fields

In order to delete just a field from the entry, Use:

    public void removeFields() {
        persistenceService.remove(RemoveAttribute.withKeys(Employee_.personId, "1")
                .withKey(Employee_.employeeId, "123")
                .remove(Employee_.salary)
                .removeMapKey(Employee_.phoneNumbers, "home"));
    }

Here we have deleted salary field and an entry of ‘home’ from phone numbers.

jfirebase-map

Delete Record

To delete a complete record:

    public void deleteCompleteRecord() {
        final Employee employee = new Employee();
        employee.setEmployeeId("123");
        employee.setPersonId("1");
        persistenceService.delete(employee);
    }

You need to populate required id fields.

jfirebase-map

Query

To query you need to provide classes in order.

Let’s say you want to find all the employee with salary greater than 1000.

    public void query() {
        final List<Employee> employees = persistenceService
                .find(QueryBuilder
                        .collection(Person.class)
                        .withId("1")
                        .subCollection(Employee.class)
                        .whereGreaterThan(Employee_.salary, 1000));
        employees
                .forEach(System.out::println);
    }

Employee(employeeId=123, personId=1, joiningDate=Sun Oct 27 02:39:43 IST 2019, salary=5000, phoneNumbers={office=123-345-567, home=456-789-456}, modifiedDate=Sun Oct 27 02:39:43 IST 2019)

Find by Id

To find a record by Id:

    public void queryFindById() {
        final Employee employee = persistenceService
                .findById(QueryBuilder
                        .collection(Person.class)
                        .withId("1")
                        .subCollection(Employee.class)
                        .withId("123"));
    }

Provide the full path to fetch item by id.

Select few fields

Let’s say you want to fetch only salaries of all the employees:

    public void querySelectedFields() {
        final List<Employee> employees = persistenceService
                .find(QueryBuilder
                        .collection(Person.class)
                        .withId("1")
                        .subCollection(Employee.class)
                        .select(Employee_.employeeId, Employee_.salary));

        employees.forEach(System.out::println);

    }
Employee(employeeId=123, personId=null, joiningDate=null, salary=5000, phoneNumbers=null, modifiedDate=null)

Running queries in Transaction

You can run the transaction and execute multiple queries in it.

    public void runTransactionExample() {
        final List<Employee> updatedEmployees = persistenceService.runTransaction(
            transaction -> {
                final List<Employee> employees = transaction.get(QueryBuilder
                        .collection(Person.class)
                        .withId("1")
                        .subCollection(Employee.class));
    
                employees.stream()
                        .peek(emp -> emp.setSalary(6000))
                        .forEach(transaction::set);
                return employees;
        });

        updatedEmployees
                .forEach(System.out::println);
    }
Employee(employeeId=123, personId=1, joiningDate=Sun Oct 27 02:44:21 IST 2019, salary=6000, phoneNumbers={office=123-345-567, home=456-789-456}, modifiedDate=Sun Oct 27 02:54:21 IST 2019)

Batching

You can batch multiple statements and commit them at once.

    public void runBatchExample() {
            persistenceService.writeInBatch(writeBatch -> {
                writeBatch.set(AttributeValue
                        .with(Person_.personId, "2") // Required field
                        .with(Person_.name, "Jon") // Required field
                        .with(Person_.age, 25)
                        .build());
    
                writeBatch.set(AttributeValue
                        .with(Employee_.employeeId, "123") // Required field
                        .with(Employee_.personId, "2") // Required field
                        .with(Employee_.phoneNumbers, "home", "222-222-222")
                        .build());
            });
        }

You can find the code example here

If you liked this article, you can buy me a coffee

Categories:

Updated:

Kumar Rohit
WRITTEN BY

Kumar Rohit

I like long drives, bike trip & good food. I have passion for coding, especially for Clean-Code.

Leave a comment