Room Furnishings

Roughly speaking, your basic use of Room is divided into three sets of classes:

  1. Entities, which are simple objects that model the data you are transferring into and out of the database
  2. The data access object (DAO), that provides the description of the Java/Kotlin API that you want for working with certain entities
  3. 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:

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:

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.

You can learn more about 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:

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:

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:

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.