Check out why Room is a Retrofit for SQLite - pt. 2

In Part 1 of our article, we introduced the basic functionalities of Room and its similarities with Retrofit (if you haven’t read it yet, it’s available here).
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.