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 extendsRoomDatabase
. 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 Note
s 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
instead.
@Insert( onConflict = OnConflictStrategy.REPLACE ) fun insertNote(note: Note): Long @Insert( onConflict = OnConflictStrategy.ABORT ) fun insertNotes(notes: List): List
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
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
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
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.
-
Android SDKAndroid Architecture Components: LiveData
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@Query("SELECT * FROM weather " + "WHERE city LIKE :city " + "ORDER BY date DESC LIMIT 1") fun findByCity(city: String ): LiveData @Query("SELECT * FROM weather " + "WHERE date < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List }
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> { info("getWeatherByCity: $city") return openWeatherService.getWeatherByCity(city) } fun saveOnDb( weatherMain: WeatherMain ) { info("saveOnDb:n$weatherMain") weatherDAO.insert( weatherMain ) } fun getRecentWeather(): LiveData { info("getRecentWeather") return weatherDAO.findLast() } fun getRecentWeatherForLocation(location: String): LiveData { 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= 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> = Transformations.switchMap( location, { l -> info("weatherByLocation: nlocation: $l") doAsync { repository.clearOldData() } return@switchMap repository.getWeatherByLocation(l) } ) private var weatherByCityResponse: LiveData > = 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+!
-
Android SDKSimplify Android App Development With Anko
-
Android SDKHow to Code Natural Language Processing on Android With IBM Watson
-
KotlinKotlin From Scratch: More Fun With Functions
-
Android SDKConcurrency and Coroutines in Kotlin
Powered by WPeMatico