Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
Apps that handle non-trivial amounts of structured data can benefit greatly from persisting that data locally. The most common use case is to cache relevant pieces of data. That way, when the device cannot access the network, the user can still browse that content while they are offline. Any user-initiated content changes are then synced to the server after the device is back online.
The core framework provides built-in support for working with raw SQL content. Although these APIs are powerful, they are fairly low-level and require a great deal of time and effort to use:
- There is no compile-time verification of raw SQL queries. As your data graph changes, you need to update the affected SQL queries manually. This process can be time consuming and error prone.
- You need to use lots of boilerplate code to convert between SQL queries and Java data objects.
Room takes care of these concerns for you while providing an abstraction layer over SQLite.
There are 3 major components in Room:
-
Database: You can use this component to create a database holder. The annotation defines the list of entities, and the class's content defines the list of data access objects (DAOs) in the database. It is also the main access point for the underlying connection.
The annotated class should be an abstract class that extends
RoomDatabase
. At runtime, you can acquire an instance of it by callingRoom.databaseBuilder()
orRoom.inMemoryDatabaseBuilder()
. -
Entity: This component represents a class that holds a database row. For each entity, a database table is created to hold the items. You must reference the entity class through the
entities
array in theDatabase
class. Each field of the entity is persisted in the database unless you annotate it with@Ignore
.Note: Entities can have either an empty constructor (if the DAO class can access each persisted field) or a constructor whose parameters contain types and names that match those of the fields in the entity. Room can also use full or partial constructors, such as a constructor that receives only some of the fields.
-
DAO: This component represents a class or interface as a Data Access Object (DAO). DAOs are the main component of Room and are responsible for defining the methods that access the database. The class that is annotated with
@Database
must contain an abstract method that has 0 arguments and returns the class that is annotated with@Dao
. When generating the code at compile time, Room creates an implementation of this class.Important: By accessing a database using a DAO class instead of query builders or direct queries, you can separate different components of your database architecture. Furthermore, DAOs allow you to easily mock database access as you test your app.
These components, along with their relationships with the rest of the app, appear in Figure 1:
The following code snippet contains a sample database configuration with 1 entity and 1 DAO:
User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}
UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
After creating the files above, you get an instance of the created database using the following code:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
Entities
When a class is annotated with @Entity
and is referenced in the entities
property of a @Database
annotation, Room creates a database table for that
entity in the database.
By default, Room creates a column for each field that's defined in the entity.
If an entity has fields that you don't want to persist, you can annotate them
using @Ignore
, as shown in the following code snippet:
@Entity class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; }
To persist a field, Room must have access to it. You can make a field public, or you can provide a setter and getter for it. If you use setter and getter methods, keep in mind that they're based on Java Beans conventions in Room.
Primary key
Each entity must define at least 1 field as a primary key. Even when there is
only 1 field, you still need to annotate the field with the @PrimaryKey
annotation. Also, if you want Room to assign automatic IDs to entities, you can
set the @PrimaryKey
's autoGenerate
property. If the entity has a composite
primary key, you can use the primaryKeys
property of the @Entity
annotation,
as shown in the following code snippet:
@Entity(primaryKeys = {"firstName", "lastName"}) class User { public String firstName; public String lastName; @Ignore Bitmap picture; }
By default, Room uses the class name as the database table name. If you want the
table to have a different name, set the tableName
property of the @Entity
annotation, as shown in the following code snippet:
@Entity(tableName = "users") class User { ... }
Caution: Table names in SQLite are case insensitive.
Similar to the tablename
property, Room uses the field names as the column
names in the database. If you want a column to have a different name, add the
@ColumnInfo
annotation to a field, as shown in the following code snippet:
@Entity(tableName = "users") class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
Indices and uniqueness
Depending on how you access the data, you might want to index certain fields in
the database to speed up your queries. To add indices to an entity, include the
indices
property within the @Entity
annotation, listing the names of the
columns that you want to include in the index or composite index. The following
code snippet demonstrates this annotation process:
@Entity(indices = {@Index("name"), @Index("last_name", "address")}) class User { @PrimaryKey public int id; public String firstName; public String address; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
Sometimes, certain fields or groups of fields in a database must be unique. You
can enforce this uniqueness property by setting the unique
property of an
@Index
annotation to true
. The following code sample prevents a table from
having two rows that contain the same set of values for the firstName
and
lastName
columns:
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
Relationships
Because SQLite is a relational database, you can specify relationships between objects. Even though most ORM libraries allow entity objects to reference each other, Room explicitly forbids this. For more details, see Addendum: No object references between entities.
Even though you cannot use direct relationships, Room still allows you to define Foreign Key constraints between entities.
For example, if there's another entity called Book
, you can define its
relationship to the User
entity using the @ForeignKey
annotation, as shown
in the following code snippet:
@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id")) class Book { @PrimaryKey public int bookId; public String title; @ColumnInfo(name = "user_id") public int userId; }
Foreign Keys are very powerful, as they allow you to specify what occurs when
the referenced entity is updated. For instance, you can tell SQLite to delete
all books for a user if the corresponding instance of User
is deleted by
including onDelete = CASCADE
in the @ForeignKey
annotation.
Nested objects
Sometimes, you'd like to express an entity or plain old Java object (POJOs) as a
cohesive whole in your database logic, even if the object contains several
fields. In these situations, you can use the @Embedded
annotation to represent
an object that you'd like to decompose into its subfields within a table. You
can then query the embedded fields just as you would for other individual
columns.
For instance, our User
class can include a field of type Address
, which
represents a composition of fields named street
, city
, state
, and
postCode
. To store the composed columns separately in the table, include an
Address
field in the User
class that is annotated with @Embedded
, as shown
in the following code snippet:
class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; }
The table representing a User
object then contains columns with the following
names: id
, firstName
, street
, state
, city
, and post_code
.
If an entity has multiple embedded fields of the same type, you can keep each
column unique by setting the prefix
property. Room then adds the provided
value to the beginning of each column name in the embedded object.
Data Access Objects (DAOs)
The main component in Room is the Dao
class. DAOs abstract access to the
database in a clean way.
Methods for convenience
There are multiple convenience queries that you can represent using a DAO class. This document includes several common examples.
Insert
When you create a DAO method and annotate it with @Insert
, Room generates an
implementation that inserts all parameters into the database in a single
transaction.
The following code snippet shows several example queries:
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
If the @Insert
method receives only 1 parameter, it can return a long
,
which is the new rowId
for the inserted item. If the parameter is an array or
a collection, it should return long[]
or List<Long>
instead.
For more details, see the reference documentation for the @Insert
annotation,
as well as the SQLite documentation for rowid
tables.
Update
Update is a convenience method that updates a set of entities, given as parameters, in the database. It uses a query that matches against the primary key of each entity. The following code snippet demonstrates how to define this method:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
Although usually not necessary, you can have this method return an int
value
instead, indicating the number of rows updated in the database.
Delete
This convenience API deletes a set of entities, given as parameters, from the database. It uses the primary keys to find the entities to delete. The following code snippet demonstrates how to define this method:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
Although usually not necessary, you can have this method return an int
value
instead, indicating the number of rows removed from the database.
Methods using @Query
@Query
is the main annotation used in DAO classes. It allows you to perform
read/write operations on a database. Each @Query
method is verified at compile
time, so if there is a problem with the query, a compilation error occurs
instead of a runtime failure.
Room also verifies the return value of the query such that if the name of the field in the returned object doesn't match the corresponding column names in the query response, Room alerts you in one of the following two ways:
- It gives a warning if only some field names match.
- It gives an error if no field names match.
Simple queries
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
This is a very simple query that loads all users. At compile time, Room knows that it is querying all columns in the user table. If the query contains a syntax error, or if the user table doesn't exist in the database, Room displays an error with the appropriate message as your app compiles.
Passing parameters into the query
Most of the time, you need to pass parameters into queries to perform filtering operations, such as displaying only users who are older than a certain age. To accomplish this task, use method parameters in your Room annotation, as shown in the following code snippet:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
When this query is processed at compile time, Room matches the :minAge
bind
parameter with the minAge
method parameter. Room performs the match using the
parameter names. If there is a mismatch, an error occurs as your app compiles.
You can also pass multiple parameters or reference them multiple times in a query, as shown in the following code snippet:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}
Returning subsets of columns
Most of the time, you need to get only a few fields of an entity. For example, your UI might display just a user's first name and last name, rather than every detail about the user. By fetching only the columns that appear in your app's UI, you save valuable resources, and your query completes more quickly.
Room allows you to return any Java object from your queries as long as the set of result columns can be mapped into the returned object. For example, you can create the following POJO to fetch the user's first name and last name:
public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
Now, you can use this POJO in your query method:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
Room understands that the query returns values for the first_name
and
last_name
columns and that these values can be mapped into the fields of the
NameTuple
class. Therefore, Room can generate the proper code. If the query
returns too many columns, or a column that doesn't exist in the NameTuple
class, Room displays a warning.
Passing a collection of arguments
Some of your queries might require you to pass in a variable number of parameters, with the exact number of parameters not known until runtime. For example, you might want to retrieve information about all users from a subset of regions. Room understands when a parameter represents a collection and automatically expands it at runtime based on the number of parameters provided.
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
Observable queries
When performing queries, you'll often want your app's UI to update automatically
when the data changes. To achieve this, use a return value of type LiveData
in
your query method description. Room generates all necessary code to update the
LiveData
when the database is updated.
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
RxJava
Room can also return RxJava2 Publisher
and Flowable
objects from the queries
you define. To use this functionality, add the
android.arch.persistence.room:rxjava2
artifact from the Room group into your
build Gradle dependencies. You can then return objects of types defined in
RxJava2, as shown in the following code snippet:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
Direct cursor access
If your app's logic requires direct access to the return rows, you can return a
Cursor
object from your queries, as shown in the following code snippet:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
Caution: It's highly discouraged to work with the Cursor API because it doesn't guarantee whether the rows exist or what values the rows contain. Use this functionality only if you already have code that expects a cursor and that you can't refactor easily.
Querying multiple tables
Some of your queries might require access to multiple tables to calculate the
result. Room allows you to write any query, so you can also join tables.
Furthermore, if the response is an observable data type, such as Flowable
or
LiveData
, Room watches all tables referenced in the query for invalidation.
The following code snippet shows how to perform a table join to consolidate information between a table containing users who are borrowing books and a table containing data about books currently on loan:
@Dao
public interface MyDao {
@Query("SELECT * FROM book "
+ "INNER JOIN loan ON loan.book_id = book.id "
+ "INNER JOIN user ON user.id = loan.user_id "
+ "WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}
You can also return POJOs from these queries. For example, you can write a query that loads a user and their pet's name as follows:
@Dao
public interface MyDao {
@Query("SELECT user.name AS userName, pet.name AS petName "
+ "FROM user, pet "
+ "WHERE user.id = pet.user_id")
public LiveData<List<UserPet>> loadUserAndPetNames();
// You can also define this class in a separate file, as long as you add the
// "public" access modifier.
static class UserPet {
public String userName;
public String petName;
}
}
Using type converters
Room provides built-in support for primitives and their boxed alternatives.
However, you sometimes use a custom data type whose value you would like to
store in the database in a single column. To add this kind of support for custom
types, you provide a TypeConverter
, which converts a custom class to and from
a known type that Room
can persist.
For example, if we want to persist instances of Date
, we can
write the following TypeConverter
to store the equivalent Unix timestamp in
the database:
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
The preceding example defines 2 functions, one that converts a
Date
object to a Long
object and another that performs the
inverse conversion, from Long
to Date
. Since Room already
knows how to persist Long
objects, it can use this converter to persist values
of type Date
.
Next, you add the @TypeConverters
annotation to the AppDatabase
class so
that Room can use the converter that you've defined for each entity and DAO in
that AppDatabase
:
AppDatabase.java
@Database(entities = {User.java}, version = 1) @TypeConverters({Converter.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }
Using these converters, you can then use your custom types in other queries, just as you would use primitive types, as shown in the following code snippet:
User.java
@Entity
public class User {
...
private Date birthday;
}
UserDao.java
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
You can also limit the @TypeConverters
to different scopes, including
individual entities, DAOs, and DAO methods. For more details, see the reference
documentation for the @TypeConverters
annotation.
Database migration
As you add and change features in your app, you need to modify your entity classes to reflect these changes. When a user updates to the latest version of your app, you don't want them to lose all of their existing data, especially if you can't recover the data from a remote server.
Room allows you to write Migration
classes to preserve user data in this
manner. Each Migration
class specifies from
and to
versions. At runtime,
Room runs each Migration
class's migrate()
method, using the correct order
to migrate the database to a later version.
Caution: If you don't provide the necessary migrations, Room rebuilds the database instead, which means you'll lose all of your data in the database.
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};
Caution: To keep your migration logic functioning as expected, use full queries instead of referencing constants that represent the queries.
After the migration process finishes, Room validates the schema to ensure that the migration occurred correctly. If Room finds a problem, it throws an exception that contains the mismatched information.
Testing migrations
Migrations aren't trivial to write, and failure to write them properly could cause a crash loop in your app. To preserve your app's stability, you should test your migrations beforehand. Room provides a testing Maven artifact to assist with this testing process. However, for this artifact to work, you need to export your database's schema.
Exporting schemas
Upon compilation, Room exports your database's schema information into a JSON
file. To export the schema, set the room.schemaLocation
annotation processor
property in your build.gradle
file, as shown in the following code snippet:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
You should store the exported JSON files—which represent your database's schema history—in your version control system, as it allows Room to create older versions of your database for testing purposes.
To test these migrations, add the android.arch.persistence.room:testing Maven artifact from Room into your test dependencies, and add the schema location as an asset folder, as shown in the following code snippet:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
The testing package provides a MigrationTestHelper
class, which can read these
schema files. It is also a Junit4 TestRule
class, so it can manage created
databases.
A sample migration test appears in the following code snippet:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db has schema version 1. insert some data using SQL queries.
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);
// Prepare for the next version.
db.close();
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
Testing your database
When running tests for your app, you shouldn't need to create a full database if you are not testing the database itself. Room allows you to easily mock the data access layer in your tests. This process is possible because your DAOs don't leak any details of your database. When testing the rest of your application, you should create mock or fake instances of your DAO classes.
There are 2 ways to test your database:
- On your host development machine.
- On an Android device.
Testing on your host machine
Room uses the SQLite Support Library, which provides interfaces that match those in the Android Framework classes. This support allows you to pass custom implementations of the support library to test your database queries.
Even though this setup allows your tests to run very quickly, it isn't recommended because the version of SQLite running on your device—and your users' devices—might not match the version on your host machine.
Testing on an Android device
The recommended approach for testing your database implementation is writing a JUnit test that runs on an Android device. Because these tests don't require creating an activity, they should be faster to execute than your UI tests.
When setting up your tests, you should create an in-memory version of your database to make your tests more hermetic, as shown in the following example:
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}
@After
public void closeDb() throws IOException {
mDb.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
For more information about testing your database migrations, see Migration Testing.
Addendum: No object references between entities
Mapping relationships from a database to the respective object model is a common practice and works very well on the server side where it's performant to lazily load fields as they're accessed.
However, on the client side, lazy loading is not feasible because it's likely to happen on the UI thread, and querying information on disk in the UI thread creates significant performance problems. The UI thread has about 16 ms to calculate and draw an activity's updated layout, so even if a query takes only 5 ms, it's still likely that your app will run out of time to draw the frame, causing noticeable jank. Worse still, the query could take more time to complete if there's a separate transaction running in parallel, or if the device is busy with other disk-heavy tasks. If you don't use lazy loading, however, the app fetches more data than it needs, creating memory consumption problems.
ORMs usually leave this decision to developers so that they can do whatever is best for their app's use cases. Unfortunately, developers usually end up sharing the model between their app and the UI. As the UI changes over time, problems occur that are difficult to anticipate and debug.
For example, take a UI that loads a list of Book
objects, with each book
having an Author
object. You might initially design your queries to use lazy
loading such that instances of Book
use a getAuthor()
method to return the
author. The first invocation of the getAuthor()
call queries the database.
Some time later, you realize that you need to display the author name in your
app's UI, as well. You can add the method call easily enough, as shown in the
following code snippet:
authorNameTextView.setText(user.getAuthor().getName());
However, this seemingly innocent change causes the Author
table to be queried
on the main thread.
If you query author information eagerly, it becomes difficult to change how data
is loaded if you no longer need that data, such as the case where your app's UI
no longer needs to display information about a particular Author
. Therefore,
your app must continue to load data that it no longer displays. This situation
is even worse if the Author
class references another table, such as with a
getBooks()
method.
For these reasons, Room disallows object references between entity classes. Instead, you must explicitly request the data that your app needs.