Check out why Room is a Retrofit for SQLite

Photo of Rafał Naniewicz

Rafał Naniewicz

Updated Feb 8, 2024 • 15 min read

Google I/O 2017 was quite revolutionary. We’ve seen a lot of amazing new products announced and heard that Google will be investing a lot into machine learning-related technologies.

This year’s I/O was also extraordinary for Android developers, as we’ve finally received official support for Kotlin (yay!) and a brand new collection of Android libraries called “Android Architecture Components”.

Now the time has come to test the new solutions. Earlier, we looked into new lifecycle components, and now we are going to take a deep dive into the Room Persistence Library which has the potential of becoming a new Retrofit for local storage – not only because of its popularity but also the similarities between the two that you will spot while using it.

The Old Way

Currently, clean SQLite is almost forgotten and does not really exist in commercial projects. Even though it is still the only database solution suggested by official documentation and gives you great (if not the best) control over local storage, the price of its usage is just too high. The reason for it is that developers end up producing lots of boilerplate writing all of the queries and then converting them to Java objects. This can be problematic for people with no SQL knowledge, not to mention the lack of default support for any reactive components, which was the big advantage of some ORMs.

Mistakes are bound to occur in the process of writing SQL queries, and the worst thing is that you won’t get to know that until you compile your app and try to execute your query. Some of those issues were addressed in popular libs like SQLBrite and SQLDelight, but working with these libraries still wasn’t easy, and the learning curve was exponential. Because of this, the number of persistence-related libs on Android is high, and we are spoilt for choice.

At Netguru, we tested lots of available solutions, and as a result of this research, we’ve settled mostly with Realm and greenDAO, which became part of our default stack. We have embraced their advantages and accepted their disadvantages, but time goes by, and new solutions arise, so we’ve decided to test Room.

You Know Retrofit? Room Will Be a Piece of Cake.

Just like Retrofit builds an abstraction over HTTP calls, Room builds an abstraction over SQL queries. To create a Retrofit and set up a client, you use Retrofit.Builder(). With the use of the client, you generate Retrofit call implementations by calling retrofit.create(...) and simply providing it with an interface describing your calls, which is done using various annotations.

Room takes a similar approach, but DAO interfaces are registered in a class extending RoomDatabase. Then you build your class by calling Room.databaseBuilder(...) and passing an abstract class extending RoomDatabase. Room will generate proper implementations of your interfaces just like Retrofit does, and you will be able to access those the through the implementation of your database class returned by the builder. It’s even possible to share data access interfaces between Retrofit and Room by defining the same interface for remote and local storage access.

Still, it isn’t much more than an interesting fact, because it will work well in very few cases, and you should be aware of drawbacks and limitations of this solution. Just like Retrofit, Room provides support for RxJava2.

What Can Room Do?

It will execute and convert queries to Java objects for you in a way that might be similar to what you know from Retrofit and Gson. It will create tables for you, so no more create table queries. It will allow you to easily build reactive streams of data.

What Can’t Room Do?

Room won’t handle object references for you (although it is possible to embed an object into a table by using @Embedded annotation for our class type). It is a feature of Room’s design, so you are not allowed to put any fields reflecting relations between objects – it has to be done by referencing primary key.

In our case, it meant that we weren’t able to create a list of ChecklistItemDb’s in TaskDb to reflect our one-to-many relationship. Why? Some ORMs (such as greenDAO, which allows you to create object dependencies) are lazily initialized, the outcome of which is that when you call a getter returning another object in a relation, you are actually making another query. Poor design might cause a developer or code reviewer to not even notice the moment such a call is made on the UI thread, and we all know that making such calls is not a good idea because it might cause framerate drops.

Actually, Room is well secured against calls on the UI thread, and by default, you won't be able to make any – dare to do so and an exception will be thrown causing a crash. This is a place where RxJava might come in handy, because it allows the delegation of all work to another thread easily. Of course, the choice is yours, and you might use any other solution known to you.

Another thing Room won’t stop you from doing is writing SQL queries, but there will be much fewer of them, and they will be much more friendly thanks to the built-in annotations and compile time checks. This will allow you to experience the full power of SQLite without a hassle.

There’s More

We went through the basic functionalities of Room and its similarities to Retrofit, but this is not the end. Now we are going to dig into implementation.

Where Do I Start?

Currently, Room is still in alpha, but it’s already available on Google’s maven repository. In order to pull it, add Google’s maven repository:


allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}

Then, add proper artifacts to your dependencies:


dependencies {
...
compile "android.arch.persistence.room:runtime:1.0.0-alpha6"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha6"
compile "android.arch.persistence.room:rxjava2:1.0.0-alpha6"
...
}

That’s it. We are now ready to go!

The New Core of Our Database

A class extending RoomDatabase is the place where you will be setting up database properties and registering DAOs interfaces, entities, and custom type converters.


@Database(version = 1, entities = {TaskDb.class, ChecklistItemDb.class})
@TypeConverters(LocalDateConverter.class)
public abstract class AppDatabase extends RoomDatabase {

public abstract TasksDao tasksDao();

public abstract ChecklistDao checklistDao();
}

Its instance can be created using Room.databaseBuilder(...). Remember that creating an AppDatabase instance is fairly expensive, so you should have a go with a singleton pattern for this object. In our example, we use a dagger, which allows us to inject a single instance easily into every class that needs it.

Entities

To create a new entity, you need to annotate the class with @Entity and reference it in @Database annotation. Based on your class, a new table will be created. Room supports primitive types and Strings by default. For the rest, we will need to provide TypeConverters.

The containing class should be referenced in @TypeConvertes annotating our AppDatabase class. If we want to use a custom name for the column representing our field, we can override it using @ColumnInfo(name = “some_name”). It is similar to what we would do in Gson when using @SerializedName annotation or @Json(name = "name") when using Moshi.


@Entity(
tableName = "check_list_item",
indices = @Index("task_id"),
foreignKeys = @ForeignKey(
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE,
entity = TaskDb.class,
parentColumns = "id",
childColumns = "task_id"
))
public class ChecklistItemDb {

@PrimaryKey(autoGenerate = true)
private final long id;

@ColumnInfo(name = "task_id")
private final long taskId;

@ColumnInfo(name = "first_name")
private final String name;

...
}

Like in the example above, we can also define a foreign key, which allows us to define the action Room should take in onDelete, for instance. You might have noticed the lack of cascade delete in some ORMs, but here it’s back as if it had always been there.

We all love auto value and how it freed us from writing all the boilerplate when creating value classes. It seems like it could work well with Room entities, but it’s currently impossible to do so unless a new extension is created (just like in Moshi or Gson). I wasn’t the only one to notice that, and there is already an open ticket for this. The future looks bright.

Type Converters

To handle types other than primitives and strings, you need to define type converters which will convert your objects to a type supported by SQLite. Creating those is fairly easy and boils down to annotating a converter method with @TypeConverter. In most cases, you will need to provide two converter methods from and to your object type – for reading and writing data. The annotated method can be a static or an instance method. In the latter case, an instance of the containing class will be created by Room. Always remember to register the class containing type converters in @TypeConverters annotation in your class extending RoomDatabase.


public class LocalDateConverter {

private LocalDateConverter() {
throw new AssertionError();
}

@TypeConverter()
public static LocalDate fromLong(@Nullable Long epoch) {
return epoch == null ? null : LocalDate.ofEpochDay(epoch);
}

@TypeConverter
public static Long localDateToEpoch(@Nullable LocalDate localDate) {
return localDate == null ? null : localDate.toEpochDay();
}
}
view raw

DAOs

DAOs are the place where you will be writing your queries, and you will be amazed by how much simpler writing those has now become. Compile time queries check does an amazing job, and you will be notified about every spelling mistake or wrong table names or attributes.


@Dao
public interface TasksDao {

@Insert
long insertTask(TaskDb taskDb);

@Update
int updateTask(TaskDb taskDb);

@Query("SELECT * FROM task WHERE is_done = 0")
Flowable<List<TaskDb>> getToDoTasks();

@Query("SELECT * FROM task WHERE is_done = 1")
Flowable<List<TaskDb>> getDoneTasks();

@Query("SELECT * FROM task WHERE is_done = 0 AND due_date < :localDate")
Flowable<List<TaskDb>> getTaskWithDueDateBefore(LocalDate localDate);

@Delete
void deleteTask(TaskDb taskDb);
}

Thanks to the RxJava support, we can harness its full power in a truly reactive way. After subscribing to a Flowable returned by getToDoTask, a new List of Tasks matching our query will be emitted each time the task table is updated. As expected, updating ChecklistItems which are in a One-to-Many relation with Tasks won’t result in any emission. If you need such updates, you can subscribe to ChecklistItems or combine them with Tasks and then take proper action. Currently, Room supports Flowable, Publisher, Maybe, Single or Entity types for queries, and void or int for Delete, Insert and Update. If we want to use RxJava for those operations as well, we can easily wrap those calls in our repository using Completable.fromAction(...) or Single.fromCallable(...), or even in our DAO interface by turning it into an abstract class and providing it with our custom implementation:


@Dao
public abstract class TasksDao {

@Insert
public abstract long insertTask(TaskDb taskDb);

public Completable insertTaskCompletable(TaskDb taskDb) {
return Completable.fromAction(() -> insertTask(taskDb));
}

}

As we mentioned in Part 1, it’s possible to share data access interfaces between Retrofit and Room in some cases. As our example app works fully offline, we tested this solution in a different project. Here is what our interface shared between Retrofit and Room looked like:


@Dao
public interface UserSource {

@GET("api/")
@android.arch.persistence.room.Query("SELECT * FROM user LIMIT :amount")
Flowable<List<User>> getUsersList(@Query("amount") int amount);
}

This approach might work in some trivial cases, but it has its own drawbacks such as the different behavior of Flowable for Retrofit and Room (Room registers for table changes, while Retrofit will actually return a single value), problems with more complex relationships between objects, and currently no default way to use autovalue.

Transactions, Inserts, Deletes

As mentioned earlier, Room does not handle object references, and it’s the developer's responsibility to handle them properly and ensure that data are consistent. To achieve this, transactions are used. If any insert fails, the data will be rolled back.


public Completable saveNewTask(Task task) {
return Completable.fromAction(() -> {
TaskDb taskDb = TaskMapper.toTaskDb(task);
appDatabase.beginTransaction();
try {
long taskId = appDatabase.tasksDao().insertTask(taskDb);
List<ChecklistItemDb> checklistItemDbs = ChecklistItemMapper.toChecklistItemDbList(taskId, task.getChecklistItemList());
appDatabase.checklistDao().insertAll(checklistItemDbs);
appDatabase.setTransactionSuccessful();
} finally {
appDatabase.endTransaction();
}
}
);
}

Something Is Missing…

Let’s say you want to delete all inserted tasks, but you don't want to make a query asking for every single task and then passing them to a method annotated with @Delete. Can you do it if there is no @DeleteAll annotation? The answer is yes, because you still have access to the SQLite database through Room. You can make your own queries by compiling your own statements and executing them like that:


public class TasksRepository {

private final AppDatabase appDatabase;

@Inject
public TasksRepository(AppDatabase appDatabase) {
this.appDatabase = appDatabase;
}

private void deleteAllTasks() {
appDatabase.compileStatement("DELETE FROM task").execute();
}

...
}

Conclusion

I was really amazed by how seamless working with Room was. SQL parsing does an amazing job by allowing spotting errors during the compilation time. Room is also much better secured against performance issues that the developer could introduce – it gives hints in warnings or totally prevents any operations on the main thread. Even though Room is still in alpha, we didn’t spot any critical issues, and the features that are already available work very well.

We are definitely looking forward to the release of a stable version, as Room seems to be a great tool. If you want to learn more or see the examples from this article in action, a repository is available here.

Photo of Rafał Naniewicz

More posts by this author

Rafał Naniewicz

Rafal is a student of Computer Science at the Wrocław University of Technology. During his studies,...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business