AndroidIntroduction

At Google I/O 2017 a new set of Android libraries was released: Android Architecture Components. The Room Persistence Library allows developers to create SQLite databases and query for objects with minimal effort. Room removes much of the boilerplate code for converting SQL queries to Java data objects, also providing compile-time validation of queries and entities.

More information about Room is available in the Google I/O session video, and in the Room documentation. In this post we will look at using Room in a simple note taking app, where the notes are stored in a SQLite database. The source code for the demo application is available on GitHub.

Project Setup

To begin using Room you will need to add a few dependencies to the project:

  • Add Google's maven repository to the root project gradle file:
    allprojects {
     repositories {
     jcenter()
     maven { url 'https://maven.google.com' }
     }
    }
  • Add Room dependencies to the app or module gradle file:
    compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"

Creating Tables

Tables definitions for Android Room are generated from your Java objects annotated with @Entity. Table and column names are derived from class and field names, unless overridden via the @Entity and @ColumnInfo annotations. For querying purposes, it is ideal to define these names yourself.

@Entity(tableName = "note")
public class Note {

 @PrimaryKey(autoGenerate = true)
 @ColumnInfo(name = "id")
 private long id;

 @ColumnInfo(name = "title")
 private String title;

 @ColumnInfo(name = "description")
 private String description;

 @ColumnInfo(name = "category_id")
 private Long categoryId;
 ...
}

Accessing Data

Room data can be accessed by interfaces annotated with @Dao, Room creates an implementation during compile time. During compilation, queries are checked against the actual table schemas and you will receive errors when trying to query non-existent columns and other common errors.

@Dao
public interface NoteDao {

 @Insert
 void insertAll(Note... notes);

 @Update
 void updateAll(Note... notes);

 @Query("SELECT * FROM note")
 List getAll();

 @Delete
 void deleteAll(Note... notes);
 ...
}

Insert, update and delete queries do not need to be defined when attempting to insert, update or delete by id; otherwise a custom method annotated with @Query can be defined.

Defining the Database

The RoomDatabase class defines the tables that will be present in the database, and also serves as the source of @Dao classes. Room also provides an implementation of the database class, complete with methods to create the database and basic migration, by dropping all tables and re-creating them.

@Database(entities = {Note.class, Category.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase {

 public static final String DB_NAME = "app_db";

 public abstract NoteDao getNoteDao();

 public abstract CategoryDao getCategoryDao();

}
With the three classes listed above that is enough to start using the database.

Querying Data

Room requires data to be queried on a background thread and will throw an error if accessed on the main thread. Below is an example of querying for all notes and passing the notes off to the adapter for display.

public class MainActivity extends AppCompatActivity implements ActionCallback, OnClickListener {

 private NoteAdapter adapter;
 private AppDatabase db;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 db = Room.databaseBuilder(this, AppDatabase.class, AppDatabase.DB_NAME).build();
 ...
 }

 @Override
 protected void onResume() {
 super.onResume();
 loadNotes();
 }

 private void loadNotes() {
 new AsyncTask>() {
 @Override
 protected List doInBackground(Void... params) {
 return db.getNoteDao().getCategoryNotes();
 }

 @Override
 protected void onPostExecute(List notes) {
 adapter.setNotes(notes);
 }
 }.execute();
 }
 ...
}

Joining Tables

Room @Entity classes represent your table structure and the results returned from @Dao methods are your result sets. When querying a single table the same @Entity classes may be used, however, when joining two tables you will want to use a different object to represent that particular projection. Below the note and category tables are joined to create a CategoryNote.

@Dao
public interface NoteDao {
 ...
 @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
 @Query("SELECT note.id, note.title, note.description, category.name as categoryName " +
 "FROM note " +
 "LEFT JOIN category ON note.category_id = category.id")
 List getCategoryNotes();

 @Query("SELECT note.id, note.title, note.description, note.category_id " +
 "FROM note " +
 "LEFT JOIN category ON note.category_id = category.id " +
 "WHERE note.id = :noteId")
 CategoryNote getCategoryNote(long noteId);
 ...
}

Notice the @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Room will issue a warning during compilation if the fields returned by the query do not match each field on the object. With Room, result sets are just POJOs. The CategoryNote class is used simply to retrieve the category name when retrieving a note.

public class CategoryNote extends Note {

 private String categoryName;
 ...
}

Inserting Data

Similar to retrieving data, inserting data occurs on a background thread as well.

private void saveNote() {
 Note note = new Note();
 note.setTitle(title.getText().toString().trim());
 note.setDescription(description.getText().toString().trim());
 new AsyncTask() {
 @Override
 protected Void doInBackground(Note... params) {
 db.getNoteDao().insertAll(params);
 return null;
 }

 @Override
 protected void onPostExecute(Void aVoid) {
 finish();
 }
 }.execute(note);
}

Database Migrations

Inevitably things change, and Room has some basic support for migrating your database to its next version. If your data does not need to be saved across migrations the version can simply be incremented and the old database will be dropped and a new database created. For maintaining your data across migrations, Room provides Migration classes which allow you to supply raw SQL queries to manually migrate your data. More information about Room database migrations can be found in the Room documentation.

Database Testing

Room also comes with some great features to make testing your database a lot easier. Room allows you to create an in-memory database for each test case and perform any operations needed and verify the results.

@RunWith(AndroidJUnit4.class)
public class NoteDaoTest {

 private NoteDao noteDao;
 private CategoryDao categoryDao;
 private AppDatabase db;

 @Before
 public void setUp() throws Exception {
 Context context = InstrumentationRegistry.getTargetContext();
 db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
 noteDao = db.getNoteDao();
 categoryDao = db.getCategoryDao();
 }

 @After
 public void tearDown() throws Exception {
 db.close();
 }

 @Test
 public void shouldCreateDatabase() {
 assertNotNull(db);
 }

 @Test
 public void shouldCreateDao() {
 assertNotNull(noteDao);
 assertNotNull(categoryDao);
 }

 @Test
 public void shouldInsertNote() {
 Note note = new Note();
 note.setTitle("name1");
 note.setDescription("description1");
 noteDao.insertAll(note);
 List notes = noteDao.getAll();

 assertEquals(1, notes.size());
 Note dbNote = notes.get(0);
 assertEquals(note.getTitle(), dbNote.getTitle());
 assertEquals(note.getDescription(), dbNote.getDescription());
 assertEquals(1, dbNote.getId());
 }

 @Test
 public void shouldDeleteNote() {
 Note note = new Note();
 note.setTitle("name1");
 noteDao.insertAll(note);
 List notes = noteDao.getAll();

 assertEquals(1, notes.size());
 noteDao.deleteAll(notes.get(0));
 notes = noteDao.getAll();
 assertEquals(0, notes.size());
 }
 ...
}

Conclusion & Future Development

The Room Persistence Library is currently in alpha and at the time of this blog only has a few open issues listed on the Google Issue Tracker. At the moment Room appears to be capable of handling simple use cases quite well, removing a lot of the boilerplate code required to use SQLite on Android. If you would like to see more features in Room, now is the time to enter those requests and star issues you care about in the tracker.

One drawback that immediately comes to mind is there is no support for defining your own table schema. While you can represent your table as a Java object there is currently no way to apply constraints besides foreign keys to your columns (COLLATE NOCASE, NOT NULL, DEFAULT, etc). This also applies to creating virtual tables for full text searching. Although Room handles the mapping of queries to objects for you, there are cases where your dataset is too large and should not be brought into memory all at once. Room supports returning a Cursor from a @Dao method, however, this requires you to create constants for the field names or use string literals for accessing the correct columns from the returned cursor. The goal of Room is to lower the bar of using SQLite on Android and is off to a good start for the general use case.

The sample code for this article can be found on GitHub.