Today we will talk about one of the libraries that make up the Android Architecture Components: Room

As a reminder, the Android Architecture Components is a collection of libraries that help you design robust, testable and maintainable applications. With, in addition, the management of the lifecycle ( LifecycleObserver, LifecycleOwner, … ), the persistence of data ( Room ) but also the interface of your application ( LiveData, ViewModel, … ).

Introduction

As you understand, we will talk database, SQLite and its DatabaseHandler, do execSQL , rawQuery , ContentValues , … Well not at all!

Room is an abstraction layer to SQLite that facilitates the management of your database, from its creation until the reading of your data through their update. In addition, Room allows you to cache relevant data from your model when you do not have a network and synchronize it with your server once you find it. The library also provides a set of annotations that will allow us to accelerate the development of our persistence layer without losing comprehension.

Prerequisites

Before starting I specify that the development is realized with the Kotlin language and that no explanation will be made on the syntaxes used. I also add that this tutorial can be a first experience Kotlin (as it was the case for me) and remains very understandable for the uninitiated. In addition, there will be no implementation with the  LiveData or RxJava libraries, the latter will be the subject of another tutorial.

Who says library, says addiction, we will start by adding those that will allow us to achieve the whole of this tutorial:

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

Data model

For the presentation of this tutorial I decided not to take again the only User class, with the development of its associated CRUD ( Create, Read, Update, Delete ), which I could see on most other Room presentation. But to embellish it with heritage, and add other classes as well as relationships between them.

So we will have, in this model of data, Player, and Coach who will be at the base, User. The Player and Coach will be able to integrate  Teams (a Coach and several Player ) and these will be able to meet during Match. The results of the matches will be represented by Score.

Implementation

User, Player, Coach

Team

Match, Score

The entities

Modify our classes to be able to persist them. We are 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 the indices and, in our case, to define the name of the table ( tableName ). If you do not define a name, Room will take the name of the class as lowercase. A second annotation here, @Ignore which will indicate that the field (the variable) ‘ avatar ‘ will not be present during the persistence. When reading the object from our database, the variable ‘ avatar’ will be null. Last annotation: @ColumnInfo (name = “xp” )  which allows specifying a name to the column. As for tableName, if you do not specify a column name, Room will take the name of the default variable.

wear

The uniqueness constraint is represented by the @PrimaryKey annotation. Here the choice has been 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 or vice versa). I add that it is possible to create primary keys composed of several fields. To do this, add the information directly in the @Entity annotation:

Team – Address

Two new features in this class: @Embedded and ForeignKey. The first annotation indicates that the Team class will embark an Address object (which must not be annotated as an entity). This translates to adding columns in the Team table representing the variables of the Address: address, postal, city object  (see the result below). The Team object also contains the identifier of a Coach (coachId) which will be the reference to the registration of the latter through 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 ).

Match – Score

Nothing particularly new for these two entities, except the foreign double key that refers to two Teams for the Match class. The onDelete = ForeignKey.CASCADE condition that will remove the Score associated with a Match if the Match is deleted from the database.

Relationships 

One-to-Many

During the presentation of the model, we defined that a Team had a Coach but also Player. Several possibilities are available for us to represent this relationship, such as adding a Player list 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 @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 CAD methods. The advantage of this solution is to not have a foreign key constraint in the Player class which allows us to create Player without Team.

Many-to-Many

To set up a Many-to-Many relationship, you only need to combine two concepts already discussed: PrimaryKey and ForeignKey . 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 so-called primitive data: Int , Long , String , etc … But sometimes we will have to want to store data with a custom type:  Date, List, Class, etc … For this, we will have to tell Room how to convert our custom type to 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 and realize, for the second, a transformation of the type 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 Gsonlibrary .

I give you the Converter code for the type Date , used when persisting a Match object The latter will convert the Date type  to a primitive Long(the timestamp):

The Dao

Data Access Object (DAO ) are interfaces that will 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 an 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 line updated.
  • @Delete : Deleting a persisted object. the annotation allows to return the number ( int ) of deleted line.
  • @Query ( “query” ) : Record 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 to you below a request which will recover the sum of the Score of each team for agiven Match :

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

Creation of the database

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

Application

I propose below an example of the 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 the 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 on 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. 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 Room’s support of LiveData, which allows us to see changes to our data and to be sensitive to the life cycle of the application.

References

Room Documentation

7 steps to Room