Monday, March 30, 2015

Fun with SQLite and ORMLite

Need to store complex objects in SQLite with ease?
Need document-based database like MongoDB?

It's actually very easy to do. This approach is intended to be used in Android but also works well outside. Just to be clear - this article describes how to store complex objects with tons of sub-objects, arrays and other stuff in DB without need of multiple tables and foreign keys. This is ideal approach for beginners and applications with lightly-loaded DBs.

Disclaimer: don't use it if you have simple objects, heavy-loaded DBs or if you need relations between tables. In these cases it will gain nothing but negative impact on performance.

So if you're still here lets take for example following class:
class ComplexData {
  public String id;
  public long timestamp;

  // some complex data here
}
Depending on how complex data is, it might require few tables with foreign keys. But we can go another way. Create a table with id, timestamp and text columns and write serialized object into text column. Id and timestamp columns are taken just as an example in case we need to search objects by both values.

We'll need DAO and DBO in case we're using ORMLite. DBO will look something like this:
@DatabaseTable(tableName = "complex_data")
class ComplexDataDbo {
  private ComplexData mObject;

  @DatabaseField(id = true, useGetSet = true)
  private String id;

  @DatabaseField(useGetSet = true)
  private long timestamp;

  @DatabaseField(useGetSet = true)
  private String json;

  private ComplexDataDbo() {
  }

  public ComplexDataDbo(ComplexData data) {
    mObject = data;
  }

  public ComplexData getComplexData() {
    return mObject;
  }

  public String getId() {
    return mObject != null ? mObject.id : "";
  }

  public void setId(String id) {
  }

  public long getTimestamp() {
    return mObject != null ? mObject.timestamp : 0;
  }

  public void setTimestamp(long timestamp) {
  }

  public String getJson() {
    return PackUtils.pack(mObject); // I'm using Jackson for serialization
  }

  public void setJson(String json) {
    mObject = PackUtils.unpack(ComplexData.class, json);
  }
}
Now we need to create DAO. It is very simple with help of ORMLite:
private Dao<ComplexDataDbo, String> mComplexDataDao;

// ...

ConnectionSource connection = new AndroidConnectionSource(...);

mComplexDataDao = DaoManager.createDao(connection, ComplexDataDbo.class);
TableUtils.createTableIfNotExists(connection, ComplexDataDbo.class);
Now we can add objects to DB like this:
mComplexDataDao.createOrUpdate(new ComplexDataDbo(complexData));
And fetch them:
ComplexData complexData = mComplexDataDao.queryForId(id).getComplexData();
For more advanced search we can use:
List<ComplexDataDbo> dbos = mComplexDataDao.queryBuilder()
    .offset(offset).limit(limit)
    .where().lt("timestamp", timestamp)
    .query();
Thats it! I've already used this approach with success in two project so be sure - it is tested and performs well.

Thursday, March 26, 2015

Fun with Parcelables

Imagine that you have a task to create wizard-like application on Android which consists of three pages and user must fill some information about himself on each page.

Basic implementation will contain Model and three Activities.

Model will look like this:
public class Model implements Serializable { // or Parcelable, it doesn't matter
  // Page #1 info
  public String nameFirst;
  public String nameLast;

  // Page #2 info
  public long birthday;

  // Page #3 info
  public String address;
}

And each activity will contain these lines:
public class Activity{N} extends Activity {
  
  private Model mModel;
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    mModel = (Model) getIntent().getSerializableExtra("model");
    // ...
  }

  private void goToNextScreen() {
    startActivity(new Intent(this, Activity{N+1}.class).putExtra("model", mModel));
  }

  // ...  
}

So far so good.

So whats wrong here?

Now imagine that user has filled form on screen #3 and went back to change something on screen #1. Everything seems OK until user goes forward again. All entered information on following screens will disappear. This is happening because Model is actually copied between activities and we're not updating previous Models with new information.



It can be solved, for example, by using startActivityForResult and passing newer Model as Activity's result in onBackKeyPressed method. This approche is good if we have simple data and few screens but in big projects it becames a nightmare to manage it.

We need some automated solution that is easy to use.

Lets play with Parcelables

A lot of people out there hate Parcelable. And they are not wrong - you have to write tons of code for each class for serialization and it is very easy, for example, to miss that one variable.

But Parcelable has one property that I always liked - Creator. With its help we can control not only object serialization but allocation as well.

Idea is following - after creating Model, we put it into a weak cache. During deserialization we will just take Model from that cache. If no Model is found there - just proceed with typical deserialiation. This way all Activities will share the same Model instance and if Activity was killed by Android - we just deserialize it.



For this we will need cache with weak references and base Parcelable class. For actual serialization I will use Jackson library but it can be anything (for example Serializable). For me Jackson library is a preferred way just because it is highly configurable.

Here is our base Parcelable class:
public class JsonParcelable implements Parcelable {

  private static final WeakCache<JsonParcelable> sCache = new WeakCache<>();

  @JsonIgnore
  private String mId;

  protected JsonParcelable() {
    mId = UUID.randomUUID().toString();
    sCache.put(getClass().getName() + "%" + mId, this);
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel parcel, int flags) {
    parcel.writeString(mId);
    parcel.writeString(getClass().getName());
    parcel.writeString(PackUtils.pack(this)); // serialize to JSON here
  }

  public static final Creator<JsonParcelable> CREATOR = new Creator<JsonParcelable>() {
    @Override
    public JsonParcelable createFromParcel(Parcel parcel) {
      try {
        String id = parcel.readString();
        String className = parcel.readString();
        String json = parcel.readString();

        synchronized (sCache) {
          JsonParcelable object = sCache.get(className + "%" + id);
          if (object != null) {
            return object;
          }
          return (JsonParcelable) PackUtils.unpack(Class.forName(className), json); // deserialize from JSON here

        }
      } catch (Exception ex) {
        throw new RuntimeException(ex);
      }
    }

    @Override
    public JsonParcelable[] newArray(int size) {
      return new JsonParcelable[size];
    }
  };
}
And weak cache class:
public class WeakCache<T> {

  private Map<String, Ref<T>> mObjects = new HashMap<>();
  private ReferenceQueue<T> mQueue = new ReferenceQueue<>();

  public T get(String id) {
    Ref ref;
    while ((ref = (Ref) mQueue.poll()) != null) {
      mObjects.remove(ref.id);
    }

    T object = null;

    Ref<T> refT = mObjects.get(id);
    if (refT == null || (object = refT.get()) == null) {
      mObjects.remove(id);
    }
    return object;
  }

  public T put(String id, T object) {
    mObjects.put(id, new Ref<>(id, object, mQueue));
    return object;
  }

  static class Ref<T> extends WeakReference<T> {
    public final String id;

    Ref(String id, T r, ReferenceQueue<? super T> q) {
      super(r, q);
      this.id = id;
    }
  }
}

How to use it?

It is very easy to use it. All you need to do is just to extend JsonParcelable and pass that object as Parcelable through Intent. Everything else is done automatically.

So our model will look like this:
public class Model extends JsonParcelable {
  // ...
}

To start Activity just use:
startActivity(new Intent(...).putExtra(mModel));

And to get Model inside Activity:
mModel = getIntent().getParcelableExtra("model");

Thats it!!!