Room through a complete example

Jérémy Giles
8 min readMay 18, 2018

--

In this post, we’re going to talk about one of the libraries that make up the Android Architecture Components : Room.

Android Architecture components is a part of a bigger family of “libraries, tools and architectural guidance to help make it quick and easy to build great Android apps” called Android Jetpack.

More specifically, Android Architecture Components is a collection of libraries that help you design robust, testable and maintainable applications. In addition to that, it allows the management of the lifecycle (LifecycleObserver, LifecycleOwner, …), the persistence of data (Room) but also the interface of your application (LiveData, ViewModel, …).

Introduction

You may guess that we will talk about SQLite and its DatabaseHandler, doExecSQL, rawQuery, ContentValues, … Well not at all !

ORM

Before you begin, a little reminder about what an ORM is. A Object Relational Mapping allow you to persist (save) a object in database without create an insert query. The ORM will manage this for us by performing a mapping between the object and our table in database.

Indeed, Room is an ORM for Android development, but I prefer the official definition: “is an abstraction layer to SQLite” that facilitates the management of your database through the different CRUD (Create, Read, Update, Delete) operations.

In addition, Room allows you to cache relevant data from your model when you don’t have a network and synchronizes it with your server once you find it. The Room library also provides a set of annotations that will allow us to greatly accelerate the development of our persistence layer without losing comprehension.

Requirements

Before starting, I’d like to note that the development is realized with the Kotlin language and that no explanation will be made on the Kotlin language itself. I also add that this tutorial doesn’t requires minimal knowledge of that language (as it was the case for me 😉) and remains very understandable for the uninitiated. In addition, we will not use neither LiveData nor RxJava libraries.

Before delving into the Room code, please create an Android project and add the following dependencies that add Kotlin and JSON parsing support :

Gson will be used for serialization / deserialization of some objects persisted in database.

In the next section, we will define the classes that represent the database model.

Data model

For this tutorial, I decided not to work on a single ”User” class and its associated CRUD , as we can see on some other Room tutorials. But instead, we will go further by showing inheritance, adding other classes that have relationships between them.

We will have in this model of data, Player and Coach that have User as a base class. Many Players and a Coach can integrate Team. The teams will also have an Address. Two teams will be able to meet during a Match. The results of the matches will be represented by a Score.

Thus, here are the classes of our data model: User, Player, Coach, Team, Address, Team, Match and Score. Here are their implementations using Kotlin :

Implementation

User, Player, Coach

Team, Address

Match, Score

In the next section, we will configure our classes in order to persist them using Room.

Persistence

We will now persist, in database, instances that make up our model. We will have to make changes to it: add annotations, make choices for the relationships between our objects and, of course, create our database and methods to achieve our CRUD.

The Entities

Let’s update our classes with Room annotations so that they can be persisted. We’re going to turn them into entities, that is to say, to have a representation of our object in base.

Player, Coach

As you can see, the entity transformation is done via the @Entity annotation, nothing more. It is possible to add parameters to this annotation, such as the primary (and/or foreign) key declaration, the definition of indices, an so one. In our case, we defined a custom name the Player table thanks to the “tableName” parameter. If you don’t define a name, Room will take the name of the class converted to lowercase.

A second annotation was used here, @Ignore, which indicates that the field ‘avatar’ will not be persisted. When reading the object from our database, the ‘avatar’ variable will be null.

One last annotation used is @ColumnInfo(name = “xp”) which allows to specify a name to the column. As for tableName, if you don’t specify a column name, Room will take by default the name the variable.

It’s recommended to add this annotation for all persisted fields, in order to avoid problems when refactoring, especially when a field is renamed.

User

The uniqueness constraint is represented by the @PrimaryKey annotation. Here, the choice was made to add an auto-generated identifier (autoGenerate = true) to the User class which will be common to the Player and Coach class (a Coach could become Player and vice versa).

Please note that it’s also possible to create primary keys composed of several fields. To do this, add the information directly in the @Entity annotation :

@Entity(primaryKeys = { “nomVariable1”, “nomVariable2” })

Let’s annotate more classes :

Team, Address

There are two new annotations in this class: @Embedded and @ForeignKey. The first annotation indicates that the Team class will directly contain an Address object (which must not be annotated as an entity). This translates to adding columns in the Team table that represent the variables of the Address: address, postal, city object (see result below). The Team object also contains the Coach ID (coachId) which will be the reference to the registration of the Coach thanks to the creation of a foreign key. We specify in its definition that we refer to the Coach class and its primary key ‘id’ is called ‘coachId’ in the Team class. We also add the action to perform when deleting a record (a Coach), here no action (onDelete = ForeignKey.NO_ACTION). You can find information on other actions here.

Result with @Embedded annotation

Match, Score

Nothing particularly new for these two entities, except the double foreign key that refers to two Teams for the Match class.

The onDelete = ForeignKey.CASCADE parameter allow to remove the Score associated with a Match if the latter is deleted from the database.

The Relations

One-to-Many

During the presentation of the model we defined that a Team had a Coach but also Player. There are several possibilities for us to implement this relationship, such as adding a List of Player in the Team class (which we will see with the Converter), or adding a foreign key referring to the identifier of a Team in Player. I will introduce a third solution, with the creation of an additional class, which will only require the addition of a variable ‘teamId’ in Player (var teamId: Long)

So, we create a class named here TeamAllPlayers which will contain, via the @Embedded annotation, a Team and a list of Player. The link will be made with the @Relation annotation between the ‘id’ field of a Team and the Player which contains a similar ‘teamId’ field. We will discuss the use of this class when creating CRUD methods. The advantage of this solution is to not have a foreign key constraint in the Player class which allows us to create a Player without a Team.

Many-to-Many

To set up a Many-to-Many relationship you only need to combine two concepts already discussed: PrimaryKey and ForeignKey. Let’s imagine that we make it possible for a Team to have more than one Player but that the Player can have several Teams too. So we would have a new entity that would represent this link :

The Converters

For now, we have implemented the persistence of the so-called primitive data: Int, Long, String, etc … But sometimes we may want to store data with a custom type: Date, List, Class, etc. In order to do that, we will have to tell Room how to convert our custom type to a primitive type. We will create a new PlayerConverter class with two methods: one that will convert a Player ID list into a string (in JSON format) and one second to perform the reverse operation.

Our methods carry the annotation @TypeConverter. The second methods transform a List <Long> to a string in JSON format which will have, for example, the following result: “[3, 6, 9]”. The first method performs the inverse processing and transforms the string into a list of Long. To facilitate the implementation we used the Gson library.

You can find below the code for the Converter for the type Date, used when persisting a Match object. The latter will convert the Date type to a primitive Long (the timestamp) :

Dao

Data Access Object (DAO) are interfaces that allow us to communicate and interact with our database. This is where we will implement our methods called CRUD.

The interface is annotated with @Dao and will be implemented when creating the database (below). We implement, via the method signature definition, the complete CRUD with the annotation for the action to perform:

  • @Insert: Persist a new object in database. The annotation allows the method to be able to return the identifier (Long, if autoGenerate = true) of the object in base, or a list of identifier List<Long> (or long []) in the case of a multiple persistence. I have voluntarily presented the three ways to define a persistence for the addition, you can define the following methods (update and delete) in the same way.
  • @Update: Update a persisted object. The annotation allows to return the number (int) of lines updated.
  • @Delete: Deleting a persisted object. the annotation allows to return the number (int) of deleted lines.
  • @Query: Read operation via the SQL query definition. The returned type can be, for example, a Player, a List<Player> or even a Cursor. I present you below a request which will recover the sum of the Score of each team for a given Match :

Example of reading a Cursor (We will return to the part of code MyApp.database?.matchDao() later in this tutorial) :

Database

To create our database we must both inherit our RoomDataBase class and add the @Database annotation. Then we implement as many functions as Dao interfaces and we add the entities in the @Database annotation parameters, as well as the version number of our database. Finally, we must specify the Converter that should be used for custom types.

Application

I propose below an example of implementation and use of our database. As mentioned at the beginning, I will not present an implementation with LiveData (or RxJava), which is recommended, but which would add an unnecessary part of complexity.

We specify, when creating our database, the class that defines it (MyDatabase) and the name of the latter, here “championship-db”, we can then call our object ‘database’ to interact with.

Below, an example of use with a function that allows us to create and persist a Player :

Conclusion

As we have seen, Room offers a new approach to store, read and update database data more easily. Another advantage is the verification, during compilation, of the relevance of our implementation (table name, columns, key, …), the consistency between our relationships and even the syntax of requests. Another nice feature is the support of LiveData, which allows us to observe changes to our data and to be sensitive to the life cycle of the application.

I would like to give my special thanks to Yassine Benabbas (https://medium.com/@yostane) for his proofreading.

--

--