7 Steps To Room
A step by step guide on how to migrate your app to Room
Room is a persistence library, part of the Android Architecture Components. It makes it easier to work with SQLiteDatabase objects in your app, decreasing the amount of boilerplate code and verifying SQL queries at compile time.
Do you already have an Android project that uses SQLite for data persistence? If so, you can migrate it to Room! Let’s see how by taking a pre-existing project and refactoring it to use Room, in 7 easy steps.
TL;DR: Update your gradle dependencies, create your entities, DAOs and database, replace your SQLiteDatabase calls with calls to DAO methods, test everything you’ve created or modified and remove unused classes. That’s it!
Our migration sample app shows an editable user name, stored in the database, as part of a User object. We used product flavors to showcase different implementations of the data layer:
- sqlite — Uses SQLiteOpenHelper and traditional SQLite interfaces.
- room — Replaces implementation with Room and provides migrations.
Each flavor uses the same UI layer, applying the Model-View-Presenter design pattern and working with a UserRepository class.
In the sqlite flavor, you’ll see a lot of code duplicated across every method that queries the database in the UsersDbHelper
and LocalUserDataSource
classes. The queries are constructed with the help of ContentValues, and the data returned by Cursor
objects is read column by column. All this code makes it very easy to introduce subtle bugs such as forgetting to add a column to the query or constructing model objects incorrectly from the database data.
Let’s see how Room improves our code. Initially, we just copy the classes from the sqlite
flavor and gradually modify them.
Step 1 — Update the gradle dependencies
Room’s dependencies are available via Google’s new Maven repository, simply add it to the list of repositories in your main build.gradle file:
allprojects {
repositories {
google()
jcenter()
}
}
Define your Room library version in the same file. For now, it’s in alpha, but keep an eye on our developer pages for version updates.
ext {
...
roomVersion = '1.0.0-alpha4'
}
In your app/build.gradle
file, add the dependencies for Room.
dependencies{
…implementation
“android.arch.persistence.room:runtime:$rootProject.roomVersion”annotationProcessor
“android.arch.persistence.room:compiler:$rootProject.roomVersion”androidTestImplementation
“android.arch.persistence.room:testing:$rootProject.roomVersion”}
To migrate to Room we will need to increase the database version and, in order to preserve user data, we will need to implement a Migration
class. To test the migration, we need to export the schema. For that, add the following to your app/build.gradle
file:
android {
defaultConfig {
...
// used by Room, to test migrations
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
// used by Room, to test migrations
sourceSets {
androidTest.assets.srcDirs +=
files("$projectDir/schemas".toString())
}
...
Step 2 — Update model classes to entities
Room creates a table for each class annotated with @Entity
; the fields in the class correspond to columns in the table. Therefore, the entity classes tend to be small model classes that don’t contain any logic. Our User
class represents the model for the data in the database. So let’s update it to tell Room that it should create a table based on this class:
- Annotate the class with
@Entity
and use thetableName
property to set the name of the table. - Set the primary key by adding the
@PrimaryKey
annotation to the correct fields — in our case, this is the ID of the User. - Set the name of the columns for the class fields using the
@ColumnInfo(name = “column_name”)
annotation. Feel free to skip this step if your fields already have the correct column name. - If multiple constructors are suitable, add the
@Ignore
annotation to tell Room which should be used and which not.
@Entity(tableName = "users")
public class User {
@PrimaryKey
@ColumnInfo(name = "userid")
private String mId;
@ColumnInfo(name = "username")
private String mUserName;
@ColumnInfo(name = "last_update")
private Date mDate;
@Ignore
public User(String userName) {
mId = UUID.randomUUID().toString();
mUserName = userName;
mDate = new Date(System.currentTimeMillis());
}
public User(String id, String userName, Date date) {
this.mId = id;
this.mUserName = userName;
this.mDate = date;
}
...
}
Note: For a seamless migration, pay close attention to the tables and columns names in your initial implementation and make sure you’re correctly setting them in the @Entity
and @ColumnInfo
annotations.
Step 3 — Create Data Access Objects (DAOs)
DAOs are responsible for defining the methods that access the database. In the initial SQLite implementation of our project, all the queries to the database were done in the LocalUserDataSource
file, where we were working with Cursor
objects. With Room, we don’t need all the Cursor
related code and can simply define our queries using annotations in the UserDao
class.
For example, when querying the database for all users, Room does all the “heavy lifting” and we only need to write:
@Query(“SELECT * FROM Users”)
List<User> getUsers();
Step 4 — Create the database
So far, we have defined our Users
table and its corresponding queries, but we haven’t yet created the database that brings these other pieces of Room together. To do this, we need to define an abstract class that extends RoomDatabase
. This class is annotated with @Database
, lists the entities contained in the database, and the DAOs which access them. The database version has to be increased by 1, from the initial value, so in our case, it will be 2.
@Database(entities = {User.class}, version = 2)
@TypeConverters(DateConverter.class)
public abstract class UsersDatabase extends RoomDatabase {
private static UsersDatabase INSTANCE;
public abstract UserDao userDao();
Because we want to keep the user data, we need to implement a Migration
class, telling Room what it should do when migrating from version 1 to 2. In our case, because the database schema isn’t altered, we will just provide an empty implementation:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Since we didn't alter the table, there's nothing else to do here.
}
};
Create the database object in the UsersDatabase
class, defining the database name and the migration:
database = Room.databaseBuilder(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.addMigrations(MIGRATION_1_2)
.build();
To find out more about how to implement database migrations and how they work under the hood, check out this post:
Step 5 — Update the Repository to use Room
We’ve created our database, our Users
table, and the queries, so now it’s time to use them! During this step, we’ll update the LocalUserDataSource
class to use the UserDao
methods. To do this, we’ll first update the constructor by removing the Context
and adding UserDao
. Of course, any class that instantiates LocalUserDataSource
needs to be updated, too.
Second, we’ll update the LocalUserDataSource
methods that query the database with calls to UserDao
methods. For example, the method that fetches all users now looks like this:
public List<User> getUsers() {
return mUserDao.getUsers();
}
And now: run time!
One of the best features of Room is that if you’re executing your database operations on the main thread, your app will crash, and the following exception message is shown:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
One reliable way to move I/O operations off the main thread, is to create a new Runnable
that runs on a single thread Executor
for every database query. As we’re already using this approach in the sqlite
flavor, no changes were needed.
Step 6 — On-device testing
We’ve created new classes — UserDao
and UsersDatabase
, and have modified our LocalUserDataSource
to use the Room database. Now, we need to test them!
Testing UserDao
To test the UserDao
, we need to create an AndroidJUnit4
test class. An awesome feature of Room is it’s ability to create an in-memory database. This avoids the need to clean up after every test case.
@Before
public void initDb() throws Exception {
mDatabase = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
UsersDatabase.class)
.build();
}
We also need to make sure we’re closing the database connection after each test.
@After
public void closeDb() throws Exception {
mDatabase.close();
}
To test the insertion of a User
, for example, we will insert the user and then we will check that we can indeed get that User
from the database.
@Test
public void insertAndGetUser() {
// When inserting a new user in the data source
mDatabase.userDao().insertUser(USER);
//The user can be retrieved
List<User> users = mDatabase.userDao().getUsers();
assertThat(users.size(), is(1));
User dbUser = users.get(0);
assertEquals(dbUser.getId(), USER.getId());
assertEquals(dbUser.getUserName(), USER.getUserName());
}
Testing the UserDao
usage in LocalUserDataSource
Making sure that LocalUserDataSource
still works correctly is easy, since we already have tests that cover the behavior of this class. All we need to do is create an in-memory database, acquire a UserDao
object from it, and use it as a parameter for the LocalUserDataSource
constructor.
@Before
public void initDb() throws Exception {
mDatabase = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
UsersDatabase.class)
.build();
mDataSource = new LocalUserDataSource(mDatabase.userDao());
}
Again, we need to make sure that we close the database after every test.
Testing the database migration
We discussed in detail how to implement database migration tests together with an explanation of how the MigrationTestHelper
works in this blog post:
Check out the code from the migration sample app for a more extensive example.
Step 7 — Cleanup
Remove any unused classes or lines of code that are now replaced by Room functionality. In our project, we just had to delete the UsersDbHelper
class, that was extending the SQLiteOpenHelper
class.
If you have a larger, more complex database and you want to incrementally migrate to Room, here’s how:
The amount of boilerplate, error-prone code decreased, the queries are now checked at compile time and everything is testable. In 7 easy steps we were able to migrate our existing app to Room. Check out sample app here. Let us know how your migration to Room went in the comments below.