Room
Room Basics
Room is a simple Object-Relational-Mapping (aka ORM) framework for Android.
ORMs allow you to treat database tables as types in your programming language. Some require external definitions of your database and types. Others create databases and the access code by examining your types, which may include extra metadata such as annotations.
Room takes the annotated-type approach. You define
- Entity classes
- Data-Access-Object interfaces (or abstract classes)
- Database abstract class
and Room can generate the database, tables, and code to access them.
When we walk through our movie example, we'll see how to set up the Room compiler as part of our build to process the annotated types and generate code.
Note
We're just going to show code snippets here; the setup needed for Room, as well as full example code, appears in the Movie Database Example.
Entity
Let's start by defining a simple entity using Room.
@Entity
data class Person(
@PrimaryKey var id: String = UUID.randomUUID().toString(),
var name: String,
var age: Int,
var ssn: String,
)
We're using a Kotlin Data Class to define the entity type. The @Entity
annotation tells the Room compiler to generate a database table and code to convert the class definition to/from a row in that table.
The @PrimaryKey
annotation tells the Room compiler which attribute represents the unique identifier for the entity. Here we see a Kotlin default value specification - if a caller creates a Person
instance and does not specify the id, one will be created for them by generating a random UUID
.
When you want to create a Person
, all you need to do is write
val person = Person(
name = "Scott",
age = 55,
ssn = "123-45-6789"
)
You can then use a Data Access Object to store that Person
in the database.
By default the table name will be the class name, and all columns will be the property names. You can modify this, but we won't go into that detail in this class. If you want more detail, take a look at https://developer.android.com/training/data-storage/room.
Data Access Object (DAO)
A Data Access Object defines your "CRUD" functions. "CRUD" stands for "Create, Read, Update and Delete", the four types of queries you'll use to access and modify database contents.
A simple DAO might look like
@Dao
interface PersonDao {
@Query("SELECT * FROM Person")
fun getPeople(): List<Person>
@Query("SELECT * FROM Person WHERE id = :id")
fun getPerson(id: String): Person
@Insert
fun insert(vararg people: Person)
@Update
fun update(vararg people: Person)
@Upsert
fun upsert(vararg people: Person)
@Delete
fun delete(vararg people: Person)
@Query("DELETE FROM Person WHERE id IN (:ids)")
fun delete(ids: List<String>)
}
Note
This is an interface, but you can also define it as an abstract class. The Room compiler will generate an implementation (or subclass) with the details of how to perform the declared functions.
Here we're defining some simple CRUD operations for a Person.
getPeople()
- Returns a list of allPerson
instances in the table. Note that the specified SQL is just normal SQL, using the entity as the table name.getPerson(id)
- Returns a single person with the specified id. The:id
syntax is replaced with theid
parameter passed to the function.insert(people)
- inserts the passedPerson
instances into the database. By default, this will throw an exception if a passedPerson
has anid
that's already in the table.update(people)
- updates thePerson
table with the values for each passedPerson
. Will throw an exception if any passedPerson
doesn't exist.upsert(people)
- updates or inserts to thePerson
table with the values for each passedPerson
. If thePerson
already exists, it will update; otherwise it will insert.delete(people)
- deletes the specified people from the table.delete(ids)
- deletes the people with the specified ids from the table. Note that we're using the@Query
annotation here to tailor the deletion.
Once you have a DAO instance, you just call the functions to access/modify data.
Note
All of the functions defined in this DAO are synchronous! You really don't want to call these from the user-interface thread or the UI may become "janky" (non-smooth animation, delays in user interaction, etc). Using the above DAO requires you to call the DAO functions from a different thread. We'll see how to do that shortly. We'll also see how to set up asynchronous queries in the DAO.
Note
You can define separate DAOs for separate entities, but you don't need to. If you don't have a huge number of DAO functions needed for your application, you can define them all in the same DAO interface/abstract class.
Database
But how do we get an instance of a DAO? That's where the Database class comes in.
@Database(
version = 1,
entities = [
Person::class
],
exportSchema = false
)
abstract class PersonDatabase: RoomDatabase() {
abstract val dao: PersonDao
}
You define a database class with the @Database
annotation, and the class must be abstract and extend RoomDatabase
. The Room compiler will generate a subclass that creates the actual DAO instance you'll use to run your queries.
By listing the entity classes in the @Database
annotation, Room knows which tables it should create. Room also supports database migration (when the entity definitions change), but that's a more advanced topic and not covered in this class.)
Usage
Now that all the pieces are defined, we can use the database as follows.
Note
You'd normally put this code in a View Model or Repository class, spread across multiple functions.
val database = Room
.databaseBuilder(
context,
// An Android context, like an Activity, used
// to locate the database file on the device
PersonDatabase::class.java,
// The database type to create
"PEOPLE"
// The name of the database file
)
.build()
val person1 = Person(
name = "Scott",
age = 55,
ssn = "123-45-6789"
)
val person2 = Person(
name = "Mikey",
age = 10,
ssn = "234-56-7890"
)
database.dao.insert(person1, person2)
val people = database.dao.getPeople()
person1.age++
database.dao.update(person1)
database.dao.delete(person2)
Room.databaseBuilder
creates our database for us. Be sure to use only one instance of your database! The database has caching and other code that won't work as expected if you create multiple instances! Because of this, you usually want to hold onto your database in a View Model or Repository, or create the instance using Dependency Injection.
Room.databaseBuilder
is written in Java, and because of this, you cannot use named parameters or pass the Kotlin KClass (PersonDatabase::class
) as a parameter. You must pass the Java Class
that represents the database by adding .class
. The Class
is metadata describing the type, and is using to create an instance of the type dynamically at runtime.
Once you have a database, you can access the dao and calls its functions.
Threading Issues
The problem with the code you've seen so far is that it might be invoked on the user interface thread, causing "jank", a poor-performing UI. It's important to perform the database processing on another thread. In this class, we'll use Kotlin's coroutines to perform our work.
For data-access queries, we'll use Flows
, collected in a coroutine. For everything else we'll launch a coroutine to perform the action.
I'm going to do a lot of hand-waving on coroutines at this point (imagine you're watching a video with my hands flailing about) and explain them in more detail later in the course. For now, follow the patterns I'll describe and don't worry too much about how things work behind the scenes.
For now, think of a coroutine as a helper that will run some code on a specific thread, and can switch to other threads depending on the work you need to run.
Coroutines use Dispatchers to manage their execution on different threads. The "Main" dispatcher runs code on the user-interface thread. The "IO" dispatcher runs potentially blocking code on a set of threads optimized for blocking function calls. The "Default" dispatcher runs code on other background threads.
Let's say that we want to display a message, fetch 10 items from the database, updating a progress bar and the message for each, then display a "done" message. For this type of processing, we need to update the message and progress bar on the UI thread, and fetch the items from the database on a background thread.
Our coroutine might look like
fun doStuff() {
scope.launch(Dispatchers.Main) {
showMessage("Fetching data...")
repeat(10) { n ->
val item = withContext(Dispatchers.IO) {
fetch(n)
}
showMessage(item.message)
updateProgress(n)
}
showMessage("All data fetched!")
}
}
Notice how this is just a function, and it looks like normal imperative logic. But it's actually switching threads! And we don't need to set up callbacks or other constructs to pass values around or trigger thread changes.
The withContext(...)
function switches over to the IO dispatcher to run our background work, then returns back to the Main (UI) dispatcher to update the UI.
Note
The code we'll write will be even simpler, as we'll be updating state for our UI rather than switching over the UI thread to update the UI directly.
Using Flows
Kotlin Flows
allow your coroutine to fetch new data as it becomes available. We can tweak our DAO data-access functions to return Flows
rather than just data:
@Query("SELECT * FROM Person")
fun getPeople(): Flow<List<Person>>
@Query("SELECT * FROM Person WHERE id = :id")
fun getPerson(id: String): Flow<Person>
So what does this do? We can now call getPeople()
or getPerson()
and it will immediately return a Flow
. We collect it from a coroutine
val people = database.dao.getPeople()
// returns the Flow immediately
scope.launch {
// watch the Flow for new lists when data changes
people.collect { people ->
// display the current list of people
}
}
The coroutine will ask for the next list of people to process, and suspend until a new list is available. (We'll talk more about "suspension" later, but in a nutshell, it allows the dispatcher to process other coroutines rather than blocking to wait for the next result to come in.) On the database side, a trigger is installed to serve up a new list when the data changes. The collect
will keep receiving new lists until we cancel the coroutine (when its host scope is canceled - could be when the application or current activity is closed).
One-Shot DAO Functions
But what if we just want to run a DAO function and not receive updates? Or what if the function is an insert/update/delete?
We want to call these functions and wait for the result. This call needs to be on a non-UI thread.
In our examples, we again use Kotlin coroutines. For example, let's call an update function in the DAO:
scope.launch {
database.dao.update(person1)
}
Here we kick off a coroutine to perform some work and call the update function. But what thread does this run on? That depends on the scope used to launch the coroutine, and would be a decision for the caller to make.
We could do
scope.launch(Dispatchers.IO) {
database.dao.update(person1)
}
but this has a few problems.
First, as mentioned, the caller has to choose the dispatcher. When you leave things to the caller reading the documentation that says "make sure you run this off the UI thread", some calls are going to miss it.
Second, Room cannot optimize the way that the database is accessed, because it doesn't know we're running the function in a coroutine.
So let's tell Room.