Persist your data, ActiveAndroid and Parse

data: 27 maja, 2015
czas czytania: 19 min
autor: Michał Górski

ActiveAndroid – data persistence

ActiveAndroid is an ORM library for SQLite DB, which provides simple but powerful api to interact with database on Android platform. It uses annotations and provides base class for Database Models.

Configuration

First, the library must be configured in an AndroidManifest file. We must add name and version of the database. Example is presented in the Listing no. 1.

<manifest ...>
    <application android:name="com.futureprocessing.qe.QeApp" ...>
        <meta-data
            android:name="AA_DB_NAME";
            android:value="database.db" />
        <meta-data
            android:name="AA_DB_VERSION";
            android:value="1" />
    </application>
</manifest>

Listing 1

Database version is optional, if you don’t provide that setting, the version will be set as 1.

The next step is to initialize the library. In most cases it should be done in extended android.app.Application class. Simply call ActiveAndroid.initialize(this) in onCreate() method as you can see in the Listing no. 2

public class QeApp extends Application {

    @Override
    public void onCreate() {
   	 super.onCreate();
   	 ActiveAndroid.initialize(this);
    }
}

Listing 2

There is also a method to reset the framework – ActiveAndroid.dispose(). If you call this method, don’t forget to initialize it again.

The authors of this library provide extension (com.activeandroid.app.Application) of android.app.Application class. It only calls initialize and dispose methods so we recommend to make your own class because probably other libraries will be initialized in this class too.

Tables and columns

Creation of the simple database model using ActiveAndroid required @Table, @Column annotations. The model must also extend com.activeandroid.Model.

In the Listing no. 3 there is an example of database model.

@Table(name = "Events")
public class EventDAO extends Model {

    @Column(name = "title")
    public String title;

    @Column(name = "description")
    public String description;

    @Column(name = "reminder_id")
    public long reminderId = -1;

    @Column(name = "favourite")
    public boolean isFavourite = false;

    @Column(name = "abstract")
    public String eventAbstract;

    @Column(name = "start_date")
    public DateTime startDate;

    @Column(name = "end_date")
    public DateTime endDate;

    @Column(name = "session", index = true)
    public SessionDAO session;

    @Column(name = "type")
    public EventTypeDAO type = new EventTypeDAO();
}

Listing 3

Important parts of that class:

@Table – Annotation which marks the class as database table, name attribute sets the table name. If you use this annotation, the name attribute is required. You can also change the name of primary key column using “id” attribute. Default name of primary key is “Id”.

@Column – Annotation which marks the field as a column of a table. You can set the name of the column, but if you don’t set this attribute, the name of the field will be used as column name. There is an option to set more attributes such as „notNull”, „unique”, „length”, „onUpdate”, „onDelete” etc. This annotation is required for all fields which should be in the table;as ActiveAndroid columns handling primitive types and relationships.

extends Model – Required for all database models. Creates auto-incrementing primary key id for table.ActiveAndroid Model. It also provides methods such as save() or delete().

default constructor – required. If you define your own constructor, you must define default constructor as well.

If you want to speed up application startup, you can specify all Model classes in the AndroidManifest, as you can see in the Listing no. 4. This operation can speed up application startup because in normal case startup ActiveAndroid will look for all Models.

<meta-data
    android:name="AA_MODELS";
    android:value="com.futureprocessing.qe.database.active.EventDAO,
               com.futureprocessing.qe.database.active.DatabaseUpdateDAO,
               com.futureprocessing.qe.database.active.EventTypeDAO,
               com.futureprocessing.qe.database.active.JoinEventPrelegent,
               com.futureprocessing.qe.database.active.PrelegentDAO,
               com.futureprocessing.qe.database.active.SessionDAO,
               com.futureprocessing.qe.database.active.SocialMediaAuthorDAO,
               com.futureprocessing.qe.database.active.SocialMediaPostDAO,
               com.futureprocessing.qe.database.active.SponsorDAO" />

Listing 4

If you prefer to declare models in the code, you can use Confugiration.Builder from ActiveAndroid library as it is presented in the Listing 5.

public class QEApp extends Application {

    @Override
    public void onCreate() {
   	 super.onCreate();

   	 Configuration.Builder configurationBuilder = new Configuration.Builder(this);
   	 configurationBuilder.addModelClasses(DatabaseUpdateDAO.class,
   	 EventDAO.class, EventTypeDAO.class,
   	 JoinEventPrelegent.class, PrelegentDAO.class,
   	 SessionDAO.class, SocialMediaAuthorDAO.class,
   	 SocialMediaPostDAO.class, SponsorDAO.class);

   	 ActiveAndroid.initialize(this);
    }
}

Listing 5

Simple operations

Saving and updating is provided by Model class. To save you must create new instance of the Model, assign data to its fields and call save() method. If you change something in this instance and call save() once again, the insert action will be changed to update. An example is shown in the Listing no. 6.

EventDAO event = new EventDAO();
event.title = "Sample title";
event.description = "Sample description";
event.save();

event = EventDAO.load(EventDAO.class, 1);
event.delete();

EventDAO.delete(EventDAO.class, 1);

Listing 6

The simplest method to load the Model from DB is to call static method load(EventDAO.class, 1) and pass model class and id as parameters. An example is provided in the Listing no. 6.

The simplest method to delete the Model form database is to call delete() method on instance of your Model class or call static delete(EventDAO.class, 1) method in the same way as static load() method.

To save multiple records at once, you can use Transaction.

ActiveAndroid.beginTransaction();
try {
    	for (int i = 0; i < 10; i++) {
        	EventDAO event = new EventDAO();
        	event.title = "Title " + i;
        	event.save();
    	}
    	ActiveAndroid.setTransactionSuccessful();
}
finally {
    	ActiveAndroid.endTransaction();
}

Listing 7

Queries

Queries in ActiveAndroid are constructed as Builder Pattern and look like normal SQL statement. Recommended solution is to add queries method to Model class as it will be DAO object with all queries. Also, pay attention to how we use constant fields as columns name. It will be helpful in defining queries to avoid typos.

First, look at two examples of Select action. In the Listing no. 8 there are examples of query methods. One provides a single row and another one a collection of rows. We can use methods as in SQL, for example „from”, „where”, „orderBy” etc.

@Table(name = "Prelegents")
public class PrelegentDAO extends Model {

    public static final String TABLE_NAME = "Prelegents";

    public static final String NAME = "prelegent_name";
    public static final String DETAILS = "details";
    public static final String COMPANY = "company";
    public static final String PHOTO_URI = "photo_uri";
    public static final String JOB = "job_title";

    @Column(name = NAME)
    public String name;

    @Column(name = DETAILS)
    public String details;

    @Column(name = COMPANY)
    public String company;

    @Column(name = JOB)
    public String job;

    @Column(name = PHOTO_URI)
    public String photoUri;

    public static List selectAll() {
   	 return new Select().from(PrelegentDAO.class).orderBy(NAME).execute();
    }

    public static PrelegentDAO select(long id) {
   	 return new Select().from(PrelegentDAO.class).where(ID + " = ?", id).executeSingle();
    }
}

Listing 8

You can also delete elements from database using query and builder pattern. Look at the Listing no. 9, the statement looks exactly the same as Select action.

public static void deleteAll() {
    new Delete().from(PrelegentDAO.class).execute();
}

Listing 9

Advanced queries

Sometimes, for performance reason, we want to create more complicated queries, for example, use joins. In normal case ActiveAndroid gets all models from database for us, but it may not be the most optimal solution.

We have Prelegents and Events in many to many relation. Take a look at the Listing no. 10, there is a linking table for prelegents and events.

@Table(name = "JoinEventPrelegent")
public class JoinEventPrelegent extends Model {

    public static final String JOIN_EVENT = "join_event";
    public static final String JOIN_PRELEGENT = "join_prelegent";
    public static final String TABLE_NAME = "JoinEventPrelegent";

    @Column(name = JOIN_EVENT, onDelete = Column.ForeignKeyAction.CASCADE)
    public EventDAO event;

    @Column(name = JOIN_PRELEGENT, onDelete = Column.ForeignKeyAction.CASCADE)
    public PrelegentDAO prelegent;
}

Listing 10

Now, we can query for all events belonging to prelegent using inner join with the use of query builder provided by ActiveAndroid framework. You can see how to use an inner join in the Listing no. 11.

new Select()
    .from(EventDAO.class)
    .innerJoin(JoinEventPrelegent.class)
    .on(EventDAO.TABLE_NAME + "." + EventDAO.ID + " = " + JoinEventPrelegent.TABLE_NAME + "." + JoinEventPrelegent.JOIN_EVENT)
    .where(JoinEventPrelegent.TABLE_NAME + "." + JoinEventPrelegent.JOIN_PRELEGENT + " = ?", getId())
    .orderBy(EventDAO.START_DATE)
    .execute();

Listing 11

ActiveAndroid also provides custom SQL queries using SQLiteUtils class. There are two methods executing SQL namely, “execSql” and “rawQuery”. The first one is provided for no results queries, whereas the second one for queries which return the result. In the Listing no. 12 you can find examples of uses of those methods.

SQLiteUtils.execSql("DELETE FROM Prelegents where id = ?", new String[]{"1"});

List prelegents =
    SQLiteUtils.rawQuery(PrelegentDAO.class, "SELECT * from Prelegents where id = ?", new String[]{"1"});

Listing 12

Prepopulating Database

Sometimes, we want to have prepopulated database on a start app to avoid downloading mass data from the Internet. ActiveAndroid has a very easy to use prepopulating mechanism.

To add prepopulated database to your app, you must create database file and simply copy it to assets folder in your project. Ensure that filename is the same as you define in the
AndroidManifest file. After an app installation on the device, a db file will be copied from “assets” to “/data/data/myapp/databases”.

How to create the prepopulated database?
For example, you can launch the app, download actual data from the webservice and download database file from the device. It will be easier if you have a rooted device or if you use emulator, because in that case you will have full access to app sandbox.

Database migration

Changing database in an app is a normal case, so if you want to migrate ActiveAndroid database schema there is a simple solution.

If you want to add new table, just add new class and the framework will do everything for you. On the other hand, if you want to change something in the existing table, you must write migration sql script and add it to the “assets/migration” folder in your project, and upgrade database version in the AndroidManifest. The script must be named .sql, where “NewVersion” is the upgraded database version from AndroidManifest.

ActiveAndroid will execute your migration script if its filename is greater than the old database version and smaller or equal to new database version.

Example:

Add new field to your table

@Table(name = "Session")
public class SessionDAO extends Model {

    public static final String TABLE_NAME = "Session";

    public static final String NAME = "session_name";
    public static final String ORDER = "sequence";
    public static final String NEW_COLUMN = "new_column";

    @Column(name = NAME)
    public String name;

    @Column(name = ORDER)
    public int order;
    
    @Column(name = NEW_COLUMN) // new column
    public String newColumn;
}

Upgrade database version

<manifest ...>
    <application android:name="com.futureprocessing.qe.QeApp" ...>
        <meta-data
            android:name="AA_DB_NAME";
            android:value="database.db" />
        <meta-data
            android:name="AA_DB_VERSION";
            android:value="2" />
    </application>
</manifest>

Write migration script

ALTER TABLE Session ADD COLUMN new_column TEXT;

Type serializers

Sometimes, we need to serialize custom data to database. For example ActiveAndroid handle java.util.Date, but if we want to use org.joda.time.DateTime, we need to add custom TypeSerializer to ActiveAndroid.

First, we need to implement our serializer class as you can see in the Listing 13.

public final class DateTimeSerializer extends TypeSerializer {

    public Class<?> getDeserializedType() {
   	 return DateTime.class;
    }

    public Class<?> getSerializedType() {
   	 return long.class;
    }

    public Long serialize(Object data) {
   	 if (data == null) {
   		 return null;
   	 }
   	 return ((DateTime) data).getMillis();
    }

    public DateTime deserialize(Object data) {
   	 if (data == null) {
   		 return null;
   	 }
   	 return new DateTime(data);
    }
}

Listing 13

The second thing that we must do is to add information where serializers are to AndroidManifest.

<meta-data
    android:name="AA_SERIALIZERS";
    android:value="com.futureprocessing.qe.database.active.serializer" />

After that steps, always when the ActiveAndroid finds a column of DateTime type, it will use DateTimeSerializer for serialization and deserialization of DateTime to log.

Parse

Introduction

Parse is a really powerful framework that enables easy creation of backend for an application. It also allows to easily implement push notification, analytics tracking and more. Native Android library allows for easier integration with parse backend side.

Parse provides ready, minimalistic framework for user permissions and user accounts as well as supports mechanism of sessions. It also brings help in linking users with Facebook accounts and serves easier way to connect your application to Facebook login mechanism.

Configuration

First things first, you need to setup an account at parse.com.
Adding parse to your project begins with downloading Parse SDK (you can do that under this link). For the purpose of this article you will only need to copy two jars bolts-android-1.2.0 and Parse-1.9.2 to your libs folder.

Now, you have to add required permissions to your manifest file (see Listing 14).

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Listing 14

Having this done, you need to initialize the library in the onCreate() method of your application subclass (see Listing 15). Remember to replace APPLICATION_ID and CLIENT_ID with your keys; you can find them in Keys section of settings on your projects page at parse.com (see Image 1)

@Override
public void onCreate() {
    Parse.initialize(context, APPLICATION_ID, CLIENT_ID);
}

Listing 15

Image 1

Image 1

 

Creating your model

Parse provides two ways of creating your model. One way is to simply create parse objects and save them, relying on the framework to create it for you. Another way is to visit parse.com and open Core-Data view, which allows you to manually create classes that can be used later in the code.

Object

Everything and anything related to storing data in parse revolves around ParseObject. It is worth mentioning these objects are schemeless, they store data in key-value pairs, keys being alphanumeric string. Objects created in Parse by default get four properties: id, creation time, modification time and ACL(permissions set per object basis).

Create, update, delete

Objects in Parse can be created using constructor with String parameter being a name of the class or one of the create method variants. It is also possible to create an object without data, meaning that it will have an id(previously known) but nothing else (it works as a pointer to an object), which can be useful when creating relations. It is achieved with one of createWithoutData method variants.

As mentioned earlier, when you save an object to the server and it has not been previously saved (has no corresponding class on the server side) Parse will create it for you.

Listing 16 shows how an object is created and saved.

ParseObject district = new ParseObject("District");
district.put("name", "Center");
district.save();

Listing 16

Parse provides five different methods to execute save operation. They are all listed below:

// saves object to the server, blocking execution of the calling 
// thread
save();
// saves object to the server, executing on the background thread
saveInBackground() ;
// as above and reports outcome via provided callback
saveInBackground(Save Callback callback);
// saves object to the server, when parse cannot be reached then 
// stores object locally and tries to resend it later
saveEventually();
// as above and when if operation executes before closing the 
// application then callback will be called.
saveEventually(SaveCallback callback);

Listing 17

Updating objects is as simple as setting their value and calling save()! You do not have to do anything else as Parse will figure out which fields have been changed and send only these ones. An exemplary code is in the Listing 18.

district.put("name","Downtown");
district.saveInBackground();

Listing 18

Deleting objects is achieved through delete() method, which has the same set of variants as save() method.

district.deleteInBackground();

Listing 19

It is also possible to delete single fields, with remove(String fieldName) method. After this operation the field has a value of null.

district.remove("name");
district.saveInBackground();

Listing 20

Retrieving objects

Retrieving objects from Parse resembles normal SQL query commands. Queries provide us with a way to retrieve a single object as well as their collection.

The first step is to construct a query object (ParseQuery) with a proper class (table) name. Having that object we can add both filtering and non-filtering constraints.

Constraints on a query can be applied with a variety of methods, which name starts with where i.e. whereEqualTo, whereExists, whereEndsWith, whereNotContainedIn.

As I mentioned earlier ParseQuery resembles sql select syntax and as in regular select you can restrict columns to be returned (selectKeys), sort results in ascending and descending manner (addAscendingOrder, addDescendingOrder), limit their number (setLimit) or skip first few results (setSkip).

It is also possible to create compound queries, to do this we have to utilize ParseQuery.or method. It accepts a list of queries and returns a ParseQuery. Any constraint set on the returned query act as if it was set with an AND operator. Worth mentioning is that elements of a compound query must not contain GeoPoint or non-filtering constraints.

Exemplary query for a District class is shown in the Listing 21. This query retrieves districts that have set budget and their population is greater than or equal to 180 000 people.

ParseQuery.getQuery("District")
       .whereExists("budget")
       .whereGreaterThan("population", 180000)
       .findInBackground(new FindCallback() {
           @Override
           public void done(List list, ParseException e) {
               if(e == null) {
                   // do something with a returned list of districts
               } else {
                   // handle the error
               }
           }
       });

Listing 21

And below is an example of a query for a District object with known id.

ParseQuery.getQuery("District")
       .getInBackground("xtwiqLKPbF", new GetCallback() {
           @Override
           public void done(ParseObject object, ParseException e) {
           }
       });

Listing 22

Object Relations

One-to-many

There are two ways to implement one-to-many relations either with Pointer or an array. The choice between them depends on the count of objects in the relation. General rule of thumb given by the Parse documentation is to use pointer if the many side of the relation accounts for more than 100 objects; otherwise you are probably good to go with an array.

To illustrate one-to-many relation I will be using an analogy to a truck that can be driven by many drivers and with one driver driving only one truck.

Array method

Creating an object with relation expressed as an array is relatively straightforward. The first thing to be done is to add a column of type Array to the given class (in the listing below its name is “streetsColumn”). Afterwards, we need to create a list of ParseObject type and add all of the related objects to it. The last step is to assign previously created list to the one side of the relation. This is shown in the listing 23.

ArrayList drivers= new ArrayList();
drivers.add(ParseObject.createWithoutData("Driver", "iUHcMaLwnm"));
drivers.add(ParseObject.createWithoutData("Driver", "YVNtkPTNsS"));
// truck is a ParseObject
truck.put("drivers", drivers);

Listing 23

This approach also has a hidden benefit, we can fetch all related objects while retrieving district object from the previous listing. To achieve that we need to invoke include method with a column name passed as a parameter. See listing 24.

ParseQuery.getQuery("Truck")
       .include("drivers")
       .getInBackground("", new GetCallback() {
           @Override
           public void done(ParseObject object, ParseException e) {
           }
       });

Listing 24

Pointer method

This method implies that relation is saved not in the truck object itself but in the driver objects. This is shown in the listing 25.

ParseObject johnDriver = …
ParseObject markDriver = …
johnDriver.put(“truck”,truck); 
markDriver.put(“truck”,truck);

Listing 25

Many-to-many

There are three ways (ParseRelation, arrays and join table) to implement this kind of relation.
For this relation we will discuss relationship between districts and streets.

Join table method

Approach with join table is particularly useful when we want to add some metadata to a relation. It requires creation of a new join class. Lets add a date of incorporating the street to the district to out relation. Creation of the ParseObject as well as all fields is shown in the listing 26.

ParseObject relationObject = new ParseObject(“DistrictStreetRelation”);
relationObject.put(“district”,districtObject);
relationObject.put(“street”,streetObject);
relationObject.put(“incorporationDate”,new Date());

Listing 26

Array method

Building many-to-many relations using arrays in parse looks just like creating two one-to-many relations. That being said lets see it in the code, take a look at the listing 27.

List districtsForStreet = ...
// add all districts this streets belongs to
ParseObject street = new ParseObject(“Street”);
street.put(“districts”, districtsForStreet);

List streetsForDistrict = …
// add all streets that lie within boundaries of this district
ParseObject district = new ParseObject(“District”);
district.put(“streets”, streetsForDistrict);

Listing 27

When writing queries you should use include method to make sure that results also include related objects.

ParseRelation method

Modelling relations with ParseRelation requires visit to the Data Browser on the web panel at parse.com. In the required class you should add a column of type ParseRelation defining its target type and column name. Having that done you should proceed to associate given objects.

ParseObject district = new ParseObject("District");

ParseRelation streets = book.getRelation("streets");
relation.add(ParseObject.createWithoutData("Street",""));
relation.add(mainStreet);
relation.add(firstStreet);

Listing 28

Listing 28 shows how to create previously described relation.

Working with ParseRelation

When retrieving an object with a field of type ParseRelation you must separately query for the relation objects, this is shown in the listing 29. Only then you will have an access to objects referenced in the relation.

ParseObject district = new ParseObject("District");

ParseRelation streetsRelation = book.getRelation("streets");
streetsRelation.getQuery()
.findInBackground(new FindCallback() {
   @Override
   public void done(List list, ParseException e) {
      
   }
});

Listing 29

Subclassing ParseObject

While reading listings you might have imagined that using methods such as getString, getInt, etc. to read or write is going to be inconvenient. To counter that you can subclass ParseObject class and register your subclasses before initializing Parse.

Listing 30 shows how to create such a subclass.

@ParseClassName("District")
public class DistrictDAO extends ParseObject {
   public DistrictDAO() {
   }
   public String getName() {
       return getString("name");
   }
   public void setName(String name) {
       put("name", name);
   }
   public ArrayList getStreets() {
       return (ArrayList) get("streetsArray");
   }
   public void setStreets(ArrayList streets) {
       put("streetsArray", streets);
   }
}

Listing 30

It is worth noting that previously, when creating ParseObject, we used to pass “District” as a parameter, in subclassed version it is taken care of by the @ParseClassName annotation.

Having the subclass ready you must register them with the local instance of Parse before initializing it in the application’s onCreate method. Code is shown in the listing 31.

ParseObject.registerSubclass(DistrictDAO.class);
// all other subclasses

Parse.initialize(context, "", "");

Listing 31

References

  1. ActiveAndroid
  2. Parse

Newsletter IT leaks

Dzielimy się inspiracjami i nowinkami z branży IT. Szanujemy Twój czas - obiecujemy nie spamować i wysyłać wiadomości raz na dwa miesiące.

Subscribe to our newsletter

Administratorem Twoich danych osobowych jest Future Processing S.A. z siedzibą w Gliwicach. Twoje dane będziemy przetwarzać w celu przesyłania cyklicznego newslettera dot. branży IT. W każdej chwili możesz się wypisać lub edytować swoje dane. Więcej informacji znajdziesz w naszej polityce prywatności.

Subscribe to our newsletter

Administratorem Twoich danych osobowych jest Future Processing S.A. z siedzibą w Gliwicach. Twoje dane będziemy przetwarzać w celu przesyłania cyklicznego newslettera dot. branży IT. W każdej chwili możesz się wypisać lub edytować swoje dane. Więcej informacji znajdziesz w naszej polityce prywatności.