Room Furnishings
Roughly speaking, your basic use of Room is divided into three sets of classes:
- Entities, which are simple objects that model the data you are transferring into and out of the database
- The data access object (DAO), that provides the description of the Java/Kotlin API that you want for working with certain entities
- The database, which ties together all of the entities and DAOs for a single SQLite database
Entities
In many ORM systems, the entity (or that system’s equivalent) is a simple object that you happen to want to store in the database. It usually represents some part of your overall domain, so a payroll system might have entities representing departments, employees, and paychecks.
With Room, a better description of entities is that they are simple objects representing tables in your database, with one Java field/Kotlin property usually mapping to one column in that table.
From a coding standpoint, an entity is a class marked with the @Entity
annotation, such as the BookmarkEntity
class:
package com.commonsware.jetpack.bookmarker;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity
public class BookmarkEntity {
@PrimaryKey
@NonNull
public String pageUrl;
public String title;
public String iconUrl;
}
package com.commonsware.jetpack.bookmarker
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class BookmarkEntity {
@PrimaryKey
var pageUrl: String = ""
var title: String? = null
var iconUrl: String? = null
}
There is no particular superclass required for entities. The expectation is that often they will be simple objects, as we see here, where BookmarkEntity
is a plain class with no superclass.
Each of the Kotlin properties (or Java fields) of the class will map to columns in the database. Usually, this is a 1:1 mapping (each property gets its own column), though there are ways to change that if needed. By default, the column names match the names of the properties or fields, so in this case, we have three columns:
pageUrl
title
iconUrl
Besides the @Entity
annotation, the only absolute requirement of an entity is that a column be designated as the primary key, usually via the @PrimaryKey
annotation on a field or property. A primary key needs to be unique — in this case, we do not want duplicate entries for the same page URL, so we will take steps to avoid adding more than one when we add bookmarks to the database. Note that a @PrimaryKey
needs to be non-nullable — that is a SQLite requirement that Room (and its associated Lint checks) helps to enforce.
Beyond these annotations, the rest of the code in your entity class is simply in support of the app — Room does not need anything else.
DAO
“Data access object” (DAO) is a fancy way of saying “the API into the data”. The idea is that you have a DAO that provides methods for the database operations that you need: queries, inserts, updates, deletes, whatever.
In Room, the DAO is identified by the @Dao
annotation, applied to either an abstract
class or an interface
. The actual concrete implementation will be code-generated for you by the Room annotation processor.
The primary role of the @Dao
-annotated abstract
class or interface
is to have one or more methods, with their own Room annotations, identifying what you want to do with the database and your entities. In the case of Bookmarker
, we have a BookmarkStore
interface that serves in this role.
package com.commonsware.jetpack.bookmarker;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
@Dao
public interface BookmarkStore {
@Query("SELECT * FROM BookmarkEntity ORDER BY title")
LiveData<List<BookmarkEntity>> all();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void save(BookmarkEntity entity);
}
package com.commonsware.jetpack.bookmarker
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface BookmarkStore {
@Query("SELECT * FROM BookmarkEntity ORDER BY title")
fun all(): Flow<List<BookmarkEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(entity: BookmarkEntity)
}
Most, if not all, functions on a @Dao
-annotated interface will have their own Room annotations. In the case of BookmarkStore
, both of the functions have a Room annotation, indicating what sort of code Room should generate for us to serve as an implementation. We do not write the SQLite access code ourselves — instead, Room handles that, while we just use the API that we declare in the @Dao
.
@Query
One of our functions has a @Query
annotation. Typically, these are for SQL SELECT
statements, though in principle a @Query
annotation can be used for any SQL. The property of the annotation contains the SQL statement to be executed. Note that this is a SQL statement and needs to use the table and column names. By default, the table name is the name of the entity class, and the column names are the names of the fields or properties, so it looks like you are referencing the entity itself.
Room supports a wide variety of return types for a query:
- A query can return a single entity, for cases where there should be at most one result
- A query can return a list of entities, for cases where there may be many matches
- A query can return other types than entities, for cases where your SQL does not match an entity (e.g., you are using aggregation functions like
SUM
)
If a query returns those sorts of types directly, then the function will be synchronous, blocking until the database I/O is completed. If, however, the query returns the type wrapped in a reactive type, then the function will perform the database I/O asynchronously — you will get the results delivered to your observer when they are ready. Also, with reactive types, if Room thinks that the data may have been altered, any active observers will get new results delivered to them automatically, without you having to call the function again.
In this case, the Java code is using LiveData
as a reactive type, as that is native to the Jetpack and requires no additional libraries (e.g., RxJava). The Kotlin code is using Flow
from coroutines, as that is a bit more natural in Kotlin.
Flow
in the "Introducing Flows and Channels" chapter of Elements of Kotlin Coroutines!
@Insert
, @Update
, and @Delete
Our other function has an @Insert
annotation. This performs a SQL INSERT
statement, for the entity or entities provided as parameters to the function. There are also @Update
and @Delete
annotations, mapping to a SQL UPDATE
or DELETE
statement, but BookmarkStore
is not using those.
Instead, the app uses the save()
function for both inserts and updates. This works courtesy of the onConflict = OnConflictStrategy.REPLACE
property on the @Insert
annotation, which says “if there already is a row in the table for this primary key, replace it with the contents of the entity”. So, when you call save()
, either it will insert a new row or overwrite the contents of an existing row, depending on whether the pageUrl
value on the entity is already used in the table or not.
save()
is marked with the suspend
keyword. The Room annotation processor will detect this and will generate a coroutine for the implementations of save()
. Room will handle setting up the background thread for you. Without the suspend
keyword, these functions would be synchronous, blocking until the database I/O completed.
Database
In addition to entities and DAOs, you will have at least one @Database
-annotated abstract
class, extending a RoomDatabase
base class. This class knits together the database file, the entities, and the DAOs. In the case of Bookmarker
, BookmarkDatabase
fills this role:
package com.commonsware.jetpack.bookmarker;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
@Database(entities = {BookmarkEntity.class}, version = 1)
abstract class BookmarkDatabase extends RoomDatabase {
private static final String DB_NAME = "bookmarks.db";
private static volatile BookmarkDatabase INSTANCE;
synchronized static BookmarkDatabase get(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context, BookmarkDatabase.class, DB_NAME).build();
}
return INSTANCE;
}
abstract BookmarkStore bookmarkStore();
}
package com.commonsware.jetpack.bookmarker
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
private const val DB_NAME = "bookmarks.db"
@Database(entities = [BookmarkEntity::class], version = 1)
abstract class BookmarkDatabase : RoomDatabase() {
abstract fun bookmarkStore(): BookmarkStore
companion object {
@Volatile
private var INSTANCE: BookmarkDatabase? = null
@Synchronized
operator fun get(context: Context): BookmarkDatabase {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context, BookmarkDatabase::class.java, DB_NAME)
.build()
}
return INSTANCE!!
}
}
}
There are two mandatory properties on a @Database
annotation:
-
entities
, providing a list of all of the entity classes whose tables should go into this database -
version
, providing a version number for the database schema (increment this number when you ship a newer app with a newer set of entities)
Just as the @Database
annotation lists the entities, each associated @Dao
-annotated class also needs to be tied into the RoomDatabase
subclass. Specifically, you need an abstract
function that returns an instance of the @Dao
-annotated type. The name of the function can be whatever you want, as Room only cares about the return type. In a typical app, every entity and every DAO is handled by a single RoomDatabase
, but you can have more than one if needed (e.g., one from a library and one for your own app’s entities).
To retrieve an instance of the generated subclass of BookmarkDatabase
, we need to call a function on the Room
class:
-
databaseBuilder()
returns aRoomDatabase.Builder
that will create a database in the file specified by the filename parameter (DB_NAME
in the sample) -
inMemoryDatabaseBuilder()
returns aRoomDatabase.Builder
that will create an in-memory database, useful for your test code
inMemoryDatabaseBuilder()
is great for testing, as it is fast and disposable. In our case, we are using databaseBuilder()
, and using its result to set up a singleton instance of BookmarkDatabase
.
Tying It All Together
Given all of this, your code “simply” needs to:
- Obtain an instance of your
RoomDatabase
subclass - Call the function(s) on it to obtain your DAO objects (e.g.,
bookmarkStore()
) - Call the functions(s) on the DAO to perform database operations, including observing any
LiveData
results or callingsuspend
functions inside a suitable coroutine scope
We have a BookmarkRepository
which does all of that, and a bit more, as we will explore shortly.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.