Android Architecture Components: the Room Persistence Library

In this final article of the Android Architecture Components series, we’ll explore the Room persistence library, an excellent new resource that makes it a lot easier to work with databases in Android. It provides an abstraction layer over SQLite, compile-time checked SQL queries, and also asynchronous and observable queries. Room takes database operations on Android to another level.

Since this is the fourth part of the series, I’ll assume that you’re familiar with the concepts and components of the Architecture package, such as LiveData and LiveModel. However, if you didn’t read any of the last three articles, you’ll still be able to follow. Still, if you don’t know much about those components, take some time to read the series—you may enjoy it.

1. The Room Component

As mentioned, Room isn’t a new database system. It is an abstract layer that wraps the standard SQLite database adopted by Android. However, Room adds so many features to SQLite that it is almost impossible to recognize. Room simplifies all the database-related operations and also makes them much more powerful since it allows the possibility of returning observables and compile-time checked SQL queries.

Room is composed of three main components: the Database, the DAO (Data Access Objects), and the Entity. Each component has its responsibility, and all of them need to be implemented for the system to work. Fortunately, such implementation is quite simple. Thanks to the provided annotations and abstract classes, the boilerplate to implement Room is kept to a minimum.

  • Entity is the class that is being saved in the Database. An exclusive database table is created for each class annotated with @Entity.
  • The DAO is the interface annotated with @Dao that mediates the access to objects in the database and its tables. There are four specific annotations for the basic DAO operations: @Insert, @Update, @Delete, and @Query.
  • The Database component is an abstract class annotated with @Database, which extends RoomDatabase. The class defines the list of Entities and its DAOs.

2. Setting Up the Environment

To use Room, add the following dependencies to the app module in Gradle:

compile "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

If you’re using Kotlin, you need to apply the kapt plugin and add another dependency.

apply plugin: 'kotlin-kapt'
// …
    dependencies {
        // …
        kapt "android.arch.persistence.room:compiler:1.0.0"
    }

3. Entity, the Database Table

An Entity represents the object that is being saved in the database. Each Entity class creates a new database table, with each field representing a column. Annotations are used to configure entities, and their creation process is really simple. Notice how simple it is to set up an Entity using Kotlin data classes.

@Entity
data class Note(

        @PrimaryKey( autoGenerate = true )
        var id: Long?,
        var text: String?,
        var date: Long?
)

Once a class is annotated with @Entity, the Room library will automatically create a table using the class fields as columns. If you need to ignore a field, just annotate it with @Ignore. Every Entity also must define a @PrimaryKey.

Table and Columns

Room will use the class and its field names to automatically create a table; however, you can personalize the table that’s generated. To define a name for the table, use the tableName option on the @Entity annotation, and to edit the columns name, add a @ColumnInfo annotation with the name option on the field. It is important to remember that the table and column names are case sensitive.

@Entity( tableName = “tb_notes” )
data class Note(

        @PrimaryKey( autoGenerate = true )
        @ColumnInfo( name = “_id” )
        var id: Long?,
        //...
) 

Indices and Uniqueness Constraints

There are some useful SQLite constraints that Room allows us to easily implement on our entities. To speed up the search queries, you can create SQLite indices at the fields that are more relevant for such queries. Indices will make search queries way faster; however, they will also make insert, delete and update queries slower, so you must use them carefully. Take a look at the SQLite documentation to understand them better.

There are two different ways to create indices in Room. You can simply set the ColumnInfo property, index, to true, letting Room set the indices for you.

@ColumnInfo(name = "date", index = true)

var date: Long

Or, if you need more control, use the indices property of the @Entity annotation, listing the names of the fields that must compose the index in the value property. Notice that the order of items in value is important since it defines the sorting of the index table.

@Entity(
        tableName = "tb_notes",
        indices = arrayOf(
                Index(
                        value = *arrayOf("date","title"),
                        name = "idx_date_title"
                )
        )
)

Another useful SQLite constraint is unique, which forbids the marked field to have duplicate values. Unfortunately, in version 1.0.0, Room doesn’t provide this property the way it should, directly on the entity field. But you can create an index and make it unique, achieving a similar result.

@Entity(
        tableName = "tb_users",
        indices = arrayOf(
                Index(
                        value = “username”,
                        name = "idx_username",
                        unique = true
                )
        )
)

Other constraints like NOT NULL, DEFAULT, and CHECK aren’t present in Room (at least until now, in version 1.0.0), but you can create your own logic on the Entity to achieve similar results. To avoid null values on Kotlin entities, just remove the ? at the end of the variable type or, in Java, add the @NonNull annotation.

Relationship Between Objects

Unlike most object-relational mapping libraries, Room doesn’t allow an entity to directly reference another. This means that if you have an entity called NotePad and one called Note, you can’t create a Collection of Notes inside the NotePad as you would do with many similar libraries. At first, this limitation may seem annoying, but it was a design decision to adjust the Room library to Android’s architecture limitations. To understand this decision better, take a look at Android’s explanation for their approach.

Even though Room’s object relationship is limited, it still exists. Using foreign keys, it is possible to reference parent and child objects and cascade their modifications. Notice that it’s also recommended to create an index on the child object to avoid full table scans when the parent is modified.

@Entity(
        tableName = "tb_notes",
        indices = arrayOf(
                Index(
                        value = *arrayOf("note_date","note_title"),
                        name = "idx_date_title"
                ),
                Index(
                        value = *arrayOf("note_pad_id"),
                        name = "idx_pad_note"
                )
        ),
        foreignKeys = arrayOf(
                ForeignKey(
                        entity = NotePad::class,
                        parentColumns = arrayOf("pad_id"),
                        childColumns = arrayOf("note_pad_id"),
                        onDelete = ForeignKey.CASCADE,
                        onUpdate = ForeignKey.CASCADE
                )
        )
)
data class Note(

        @PrimaryKey( autoGenerate = true )
        @ColumnInfo( name = "note_id" )
        var id: Long,

        @ColumnInfo( name = "note_title" )
        var title: String?,

        @ColumnInfo( name = "note_text" )
        var text: String,

        @ColumnInfo( name = "note_date" )
        var date: Long,

        @ColumnInfo( name = "note_pad_id")
        var padId: Long
)

Embedding Objects

It is possible to embed objects inside entities using the @Embedded annotation. Once an object is embedded, all of its fields will be added as columns in the entity’s table, using the embedded object’s field names as column names. Consider the following code.

data class Location(
        var lat: Float,
        var lon: Float
)
@Entity(tableName = "tb_notes")
data class Note(
        @PrimaryKey( autoGenerate = true )
        @ColumnInfo( name = "note_id" )
        var id: Long,

        @Embedded( prefix = "note_location_" )
        var location: Location?
)

In the code above, the Location class is embedded in the Note entity. The entity’s table will have two extra columns, corresponding to fields of the embedded object. Since we’re using the prefix property on the @Embedded annotation, the columns’ names will be ‘note_location_lat’ and ‘note_location_lon’, and it will be possible to reference those columns in queries.

4. Data Access Object

To access the Room’s Databases, a DAO object is necessary. The DAO can be defined either as an interface or an abstract class. To implement it, annotate the class or interface with @Dao and you’re good to access data. Even though it is possible to access more than one table from a DAO, it is recommended, in the name of a good architecture, to maintain the Separation of Concerns principle and create a DAO responsible for accessing each entity.

@Dao
interface NoteDAO{}

Insert, Update, and Delete

Room provides a series of convenient annotations for the CRUD operations in the DAO: @Insert, @Update, @Delete, and @Query. The @Insert operation may receive a single entity, an array, or a List of entities as parameters. For single entities, it may return a long, representing the row of the insertion. For multiple entities as parameters, it may return a long[] or a List<Long> instead.

@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insertNote(note: Note): Long
    
@Insert( onConflict = OnConflictStrategy.ABORT )
fun insertNotes(notes: List<Note>): List<Long>

As you can see, there is another property to talk about: onConflict. This defines the strategy to follow in case of conflicts using OnConflictStrategy constants. The options are pretty much self-explanatory, with ABORT, FAIL, and REPLACE being the more significant possibilities.

To update entities, use the @Update annotation. It follows the same principle as @Insert, receiving single entities or multiple entities as arguments. Room will use the receiving entity to update its values, using the entity PrimaryKey as reference. However, the @Update may only return an int representing the total of table rows updated.

@Update()
fun updateNote(note: Note): Int

Again, following the same principle, the @Delete annotation may receive single or multiple entities and return an int with the total of table rows updated. It also uses the entity’s PrimaryKey to find and remove the register in the database’s table.

@Delete
fun deleteNote(note: Note): Int

Making Queries

Finally, the @Query annotation makes consultations in the database. The queries are constructed in a similar manner to SQLite queries, with the biggest difference being the possibility to receive arguments directly from the methods. But the most important characteristic is that the queries are verified at compile time, meaning that the compiler will find an error as soon as you build the project.

To create a query, annotate a method with @Query and write a SQLite query as value. We won’t pay too much attention to how to write queries since they use the standard SQLite. But generally, you’ll use queries to retrieve data from the database using the SELECT command. Selections may return single or collection values.

@Query("SELECT * FROM tb_notes")
fun findAllNotes(): List<Note>

It is really simple to pass parameters to queries. Room will infer the parameter’s name, using the method argument’s name. To access it, use :, followed by the name.

@Query("SELECT * FROM tb_notes WHERE note_id = :id")
fun findNoteById(id: Long): Note

@Query(“SELECT * FROM tb_noted WHERE note_date BETWEEN :early AND :late”)
fun findNoteByDate(early: Date, late: Date): List<Note>

LiveData Queries

Room was designed to work gracefully with LiveData. For a @Query to return a LiveData, just wrap up the standard return with LiveData<?> and you’re good to go.

@Query("SELECT * FROM tb_notes WHERE note_id = :id")
fun findNoteById(id: Long): LiveData<Note>

After that, it will be possible to observe the query result and get asynchronous results quite easily. If you don’t know the power of LiveData, take some time to read our tutorial about the component.

5. Creating the Database

The database is created by an abstract class, annotated with @Database and extending the RoomDatabase class. Also, the entities that will be managed by the database must be passed in an array in the entities property in the @Database annotation.

@Database(
        entities = arrayOf(
                NotePad::class,
                Note::class
        )
)
abstract class Database : RoomDatabase() {
    abstract fun padDAO(): PadDAO
    abstract fun noteDAO(): NoteDAO
}

Once the database class is implemented, it is time to build. It is important to stress that the database instance should ideally be built only once per session, and the best way to achieve this would be to use a dependency injection system, like Dagger. However, we won’t dive into DI now, since it is outside the scope of this tutorial.

fun providesAppDatabase() : Database {
        return Room.databaseBuilder(
                context, Database::class.java, "database")
                .build()
    }

Normally, operations on a Room database cannot be made from the UI Thread, since they are blocking and will probably create problems for the system. However, if you want to force execution on the UI Thread, add allowMainThreadQueries to the build options. In fact, there are many interesting options for how to build the database, and I advise you to read the RoomDatabase.Builder documentation to understand the possibilities.

6. Datatype and Data Conversion

A column Datatype is automatically defined by Room. The system will infer from the field’s type which kind of SQLite Datatype is more adequate. Keep in mind that most of Java’s POJO will be converted out of the box; however, it is necessary to create data converters to handle more complex objects not recognized by Room automatically, such as Date and Enum.

For Room to understand the data conversions, is necessary to provide TypeConverters and register those converters in Room. It is possible to make this registration taking into consideration specific context—for example, if you register the TypeConverter in the Database, all entities of the database will use the converter. If you register on an entity, only the properties of that entity may use it, and so on.

To convert a Date object directly to a Long during Room’s saving operations and then convert a Long to a Date when consulting the database, first declare a TypeConverter.

class DataConverters {
    @TypeConverter
    fun fromTimestamp(mills: Long?): Date? {
        return if (mills == null)
            null
        else Date(mills)
    }

    @TypeConverter
    fun fromDate(date: Date?): Long? =
            date?.time
}

Then, register the TypeConverter in the Database, or in a more specific context if you want.

@Database(
        entities = arrayOf(
                NotePad::class,
                Note::class
        ),
        version = 1
)
@TypeConverters(DataConverters::class)
abstract class Database : RoomDatabase()

7. Using Room in an App

The application we’ve developed during this series used SharedPreferences to cache weather data. Now that we know how to use Room, we’ll use it to create a more sophisticated cache that’ll allow us to get cached data by city, and also consider the weather date during the data retrieval.

First, let’s create our entity. We’ll save all our data using only the WeatherMain class. We only need to add some annotations to the class, and we’re done.

@Entity( tableName = "weather" )
data class WeatherMain(

        @ColumnInfo( name = "date" )
        var dt: Long?,

        @ColumnInfo( name = "city" )
        var name: String?,

        @ColumnInfo(name = "temp_min" )
        var tempMin: Double?,

        @ColumnInfo(name = "temp_max" )
        var tempMax: Double?,

        @ColumnInfo( name = "main" )
        var main: String?,

        @ColumnInfo( name = "description" )
        var description: String?,

        @ColumnInfo( name = "icon" )
        var icon: String?
) {
    @ColumnInfo(name = "id")
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
    // ...

We also need a DAO. The WeatherDAO will manage CRUD operations in our entity. Notice that all queries are returning LiveData.

@Dao
interface WeatherDAO {

    @Insert( onConflict = OnConflictStrategy.REPLACE )
    fun insert( w: WeatherMain )

    @Delete
    fun remove( w: WeatherMain )

    @Query( "SELECT * FROM weather " +
            "ORDER BY id DESC LIMIT 1" )
    fun findLast(): LiveData<WeatherMain>

    @Query("SELECT * FROM weather " +
            "WHERE city LIKE :city " +
            "ORDER BY date DESC LIMIT 1")
    fun findByCity(city: String ): LiveData<WeatherMain>

    @Query("SELECT * FROM weather " +
            "WHERE date < :date " +
            "ORDER BY date ASC LIMIT 1" )
    fun findByDate( date: Long ): List<WeatherMain>
}

Finally, it is time to create the Database.

@Database( entities = arrayOf(WeatherMain::class), version = 2 )
abstract class Database : RoomDatabase() {
    abstract fun weatherDAO(): WeatherDAO
}

Ok, we now have our Room database configured. All that is left to do is wire it up with Dagger and start using it. In the DataModule, let’s provide the Database and the WeatherDAO.

@Module
class DataModule( val context: Context ) {
    // ...
    @Provides
    @Singleton
    fun providesAppDatabase() : Database {
        return Room.databaseBuilder(
                context, Database::class.java, "database")
                .allowMainThreadQueries()
                .fallbackToDestructiveMigration()
                .build()

    }
    @Provides
    @Singleton
    fun providesWeatherDAO(database: Database) : WeatherDAO {
        return database.weatherDAO()
    }
}

As you should remember, we have a repository responsible for handling all data operations. Let’s continue to use this class for the app’s Room data request. But first, we need to edit the providesMainRepository method of the DataModule, to include the WeatherDAO during the class construction.

@Module
class DataModule( val context: Context ) {
    //...
    @Provides
    @Singleton
    fun providesMainRepository(
            openWeatherService: OpenWeatherService,
            prefsDAO: PrefsDAO,
            weatherDAO: WeatherDAO,
            locationLiveData: LocationLiveData
    ) : MainRepository {
        return MainRepository(
                openWeatherService,
                prefsDAO,
                weatherDAO,
                locationLiveData
        )
    }
    /…
}

Most of the methods that we’ll add to the MainRepository are pretty straightforward. It’s worth looking more closely at clearOldData(), though. This clears all data older than a day, maintaining only relevant weather data saved in the database.

class MainRepository
    @Inject
    constructor(
            private val openWeatherService: OpenWeatherService,
            private val prefsDAO: PrefsDAO,
            private val weatherDAO: WeatherDAO,
            private val location: LocationLiveData
    ) : AnkoLogger
{

    fun getWeatherByCity( city: String ) : LiveData<ApiResponse<WeatherResponse>>
    {
        info("getWeatherByCity: $city")
        return openWeatherService.getWeatherByCity(city)
    }

    fun saveOnDb( weatherMain: WeatherMain ) {
        info("saveOnDb:\n$weatherMain")
        weatherDAO.insert( weatherMain )
    }

    fun getRecentWeather(): LiveData<WeatherMain> {
        info("getRecentWeather")
        return weatherDAO.findLast()
    }

    fun getRecentWeatherForLocation(location: String): LiveData<WeatherMain> {
        info("getWeatherByDateAndLocation")
        return weatherDAO.findByCity(location)
    }

    fun clearOldData(){
        info("clearOldData")
        val c = Calendar.getInstance()
        c.add(Calendar.DATE, -1)
        // get weather data from 2 days ago
        val oldData = weatherDAO.findByDate(c.timeInMillis)
        oldData.forEach{ w ->
            info("Removing data for '${w.name}':${w.dt}")
            weatherDAO.remove(w)
        }
    }
    // ...
}

The MainViewModel is responsible for making consultations to our repository. Let’s add some logic to address our operations to the Room database. First, we add a MutableLiveData, the weatherDB, which is responsible for consulting the MainRepository. Then, we remove references to SharedPreferences, making our cache rely only on the Room database.

class MainViewModel
@Inject
constructor(
        private val repository: MainRepository
)
    : ViewModel(), AnkoLogger {
    // …
    // Weather saved on database
    private var weatherDB: LiveData<WeatherMain> = MutableLiveData()
    // …
    // We remove the consultation to SharedPreferences
    // making the cache exclusive to Room
    private fun getWeatherCached() {
        info("getWeatherCached")
        weatherDB = repository.getRecentWeather()

        weather.addSource(
                weatherDB,
                {
                    w ->
                    info("weatherDB: DB: \n$w")
                    weather.postValue(ApiResponse(data = w))
                    weather.removeSource(weatherDBSaved)
                }
        )
    }

To make our cache relevant, we’ll clear old data every time a new weather consultation is made.

    private var weatherByLocationResponse:
            LiveData<ApiResponse<WeatherResponse>> = Transformations.switchMap(
            location,
            {
                l ->
                info("weatherByLocation: \nlocation: $l")
                doAsync { repository.clearOldData() }
                return@switchMap repository.getWeatherByLocation(l)
            }
    )

    private var weatherByCityResponse:
            LiveData<ApiResponse<WeatherResponse>> = Transformations.switchMap(
            cityName,
            {
                city ->
                info("weatherByCityResponse: city: $city")
                doAsync { repository.clearOldData() }
                return@switchMap repository.getWeatherByCity(city)
            }
    )

Finally, we’ll save the data to the Room database every time new weather is received.

// Receives updated weather response,
    // send it to UI and also save it
    private fun updateWeather(w: WeatherResponse){
        info("updateWeather")
        // getting weather from today
        val weatherMain = WeatherMain.factory(w)
        // save on shared preferences
        repository.saveWeatherMainOnPrefs(weatherMain)
        // save on db
        repository.saveOnDb(weatherMain)
        // update weather value
        weather.postValue(ApiResponse(data = weatherMain))
    }

You can see the complete code in the GitHub repo for this post.

Conclusion

Finally, we’re at the conclusion of the Android Architecture Components series. These tools will be excellent companions on your Android development journey. I advise you to continue exploring the components. Try to take some time to read the documentation

And check out some of our other posts on Android app development here on Envato Tuts+!

Leave a Reply

Your email address will not be published. Required fields are marked *