In this article, we’ll consider some practical examples of how to use the Backendless Data Collection library to its fullest extent. You can find code documentation and sources for the library here.
This is an implementation of the standard Java Collection interface enabling you to retrieve, iterate, transform, perform computations over a collection of objects stored in a Backendless data table via Java collection.
Of course, that’s not all. It also has a number of features that can make your work with Backendless Data Service more convenient and seamless (from the point of working with collections). For example, it allows you to:
– iterate over all data in the table just as you would over an ordinary java collection;
– iterate over all data in the table without manual pagination;
– create a Java Iterator
object to pass through the collection;
– create a Java Stream
object and use advantages of Java stream API;
– add/remove collection operations to update the data in the table;
– use special persistence mode that allows you to work with previously received data offline;
– applicate the slice to limit the data that the collection will map to (such approach allows you to see and work with only the subset of objects).
Before we start, you will need to link two libraries:
backendless
library:
<dependency> <groupId>com.backendless</groupId> <artifactId>backendless</artifactId> <version>${bkndls.version}</version> </dependency>
and backendless-data-collection
library: Linked here.
(because it is not a part of the backendless SDK, you will need to download it manually)
<dependency> <groupId>com.backendless</groupId> <artifactId>backendless-data-collection</artifactId> <version>1.0.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/backendless-data-collection-1.0.jar</systemPath> </dependency>
1) For the examples, we’ll use a table with the following schema:
You may download and import the table schema with sample data from here.
Table name: Car
Fields:
brand | STRING |
model | STRING |
body | STRING |
color | STRING |
fuel_type | STRING |
engine_capacity | INT |
transmission | STRING |
issue_data | DATETIME |
condition | STRING |
2) Then you need to create Java classes for the data tables. Thanks to Code Generation, Backandless already does it for you. All you have to do is unpack the archive and add the Car.java
class to your project.
3) The last step that is required is to implement the interface
BackendlessDataCollection.Identifiable<>
The change is quite simple: just add an implements
declaration for class and add method setObjectId
.
public class Car implements BackendlessDataCollection.Identifiable<Car> { @Override public String getObjectId() { return objectId; } @Override public void setObjectId( String s ) { objectId = s; } }
Now we’re ready to start. Let’s see what we can do with the BackendlessDataCollection API.
Create a simple collection that represents the entire dataset from our backend table.
BackendlessDataCollection<Car> cars = new BackendlessDataCollection<>( Car.class );
Get amount of records:
// doesn't make call to backend int recordsCount = cars.size(); // invalidates state of this collection and retrieve the new size cars.invalidateState(); cars.size();
Iterate over elements in collection (table) seamlessly without pagination (every iteration that reached the end (wasn’t interrupted) refreshes the actual collection size automatically, so you no need to call method invalidateState()
if you regularly perform iteration):
for( Car car : cars ) { System.out.println( car.getBrand() + " :: " + car.getModel() ); } // or shorter cars.forEach( System.out::println );
BackendlessDataCollection supports contains
, add
and remove
operations. It also has a special method getById
that is not present in an ordinary Java collection and gives you more capabilities to use.
// If you already know objectId of the entity Car car = cars.getById( "600A5962-6686-3D62-FFCD-CCA5BAC2F800" ); // update entity and save it car.setTransmission( "auto" ); car.setEngine_capacity( 2100 ); cars.add( car ); // Add new Car into collection Car newCar = new Car(); newCar.setBrand( "BMW" ); newCar.setModel( "520M" ); newCar.setColor( "Wet Asphalt" ); newCar.setFuel_type( "diesel" ); newCar.setEngine_capacity( 2000 ); newCar.setCondition( "used" ); newCar.setTransmission( "auto" ); newCar.setIssue_date( Date.from( LocalDate.of( 2016, Month.MARCH, 10 ).atStartOfDay( ZoneId.systemDefault() ).toInstant()) ); boolean isSaved = cars.add( newCar ); // check the presence in the collection // the logic takes into account only 'objectId' Car myCar = new Car("600A5962-6686-3D62-FFCD-CCA5BAC2F800"); boolean myCarExists = cars.contains( myCar ); // you can use objects that you've gotten earlier from the collection boolean newCarExists = cars.contains( newCar ); // check the presence of several objects boolean newCarAndMyCarExist = cars.contains( Arrays.asList(myCar, newCar) ); // delete entity or multiple entities boolean isRemoved = cars.remove( newCar ); isRemoved = cars.remove( Arrays.asList(myCar, newCar) ); cars.remove( new Car("<objectId>") ); // and also very useful method will retrieve all the data from remote table into local array Car[] arrayOfCars = cars.toArray();
Do you like Java data streams? They really make it easier to process objects. And BackendlessDataCollection supports all their features.
Set<String> brands = cars.stream() .map( Car::getBrand ) .collect( Collectors.toSet() ); Date issueDate = Date.from( LocalDate.of( 2016, Month.MARCH, 10 ).atStartOfDay( ZoneId.systemDefault() ).toInstant()); List<Car> filteredCars = cars.stream() .filter( c -> "auto".equals( c.getTransmission() ) ) .filter( c -> c.getIssue_date().after( issueDate ) ) .sorted() .collect( Collectors.toList() ); Map<String, List<Car>> carsByBrand = cars.stream() .collect( Collectors.groupingBy(Car::getBrand) );
What if you need to work only with a specific collection slice or, in other words, a subset of objects in your table?
// here we use the same table, but our collection will reflect // only the particular subset of cars that have electric engine cars = new BackendlessDataCollection<>( Car.class, "fuel_type = 'electro'" );
You keep all the power described in the previous parts, but now all methods are bordered with the query you put in the constructor.
So the iteration through such collection will take into account only cars with electric engine, and the size()
method will return the number of cars only of such type.
contains()
, add()
and remove()
operations directly perform calls to the Backendless server and would be discarded if the object doesn’t match the slice clause. I.e. if you try to add the object that has ‘gasoline’ engine, the new record won’t be created in the table. Or if you try to remove an object with a ‘diesel’ engine, the operation won’t be performed.
The above examples cover the use cases where the number of objects is quite small or the data change very frequently.
Let’s dive deeper. Imagine that we have a large amount of data in our table. Thus the usage of Java streams can take a significant amount of time, network resources, and API calls.
BackendlessDataCollection gives you tools to solve such a problem — you can use persisted mode. Some operations would perform locally (without API calls to the server) and thus drastically reduce execution time.
Look closer at these opportunities.
Persisted mode is set during BackendlessDataCollection instance creation and cannot be changed further.
// set persisted mode cars = new BackendlessDataCollection<>( Car.class, true ); // set persisted mode with the slice simultaneously cars = new BackendlessDataCollection<>( Car.class, "fuel_type = 'electro'", true );
The collection is lazy, thus the data will be loaded only during iteration over it. So only the first iteration will perform calls to the Backendless server; all subsequent iterations will be done locally without any request. But you may force the refresh of the collection with the method invalidateState()
which updates the real size and clears all locally saved data so the next iteration will make requests to the server again.
Method getPersistedSize()
returns the size of locally saved data. It may be less or equal to size()
.
Method populate()
forcibly downloads all data from the table (so-called greedy initialization). If isLoaded() == true
, it does nothing.
If the iterator was interrupted, only part of the data would be saved locally and next time, the iterator will first iterate over local data and then (if needed) start making calls to the server.
We can’t wait to see what you build with this new tool.
Thanks for reading, and Happy Coding!