fbpx

Blogs from the Ranch

< Back to Our Blog

Room: Data Storage on Android for Everyone

Avatar

Andrew Bailey

Room is a new way to save application data in your Android app announced at Google I/O this year.
It’s part of the new Android Architecture components, a group of libraries from Google that support an opinionated application architecture. Room is offered as a high-level, first-party alternative to Realm, ORMLite, GreenDao and many others.

Room is a high-level interface to the low-level SQLite bindings built into Android, which you can learn more about in Android’s documentation. It does most of its work at compile-time by generating a persistence API on top of the built-in SQLite API, so you don’t have to touch a single Cursor or ContentResolver. Room lets you use SQL where it’s most powerful, for query building, while taking care of schema definition and data manupulation for you.

Using Room

First, add Room to your project. After that, you’ll need to tell Room what your data looks like. Suppose you start with a simple model class that looks like this:

public class Person {
    String name;
    int age;
    String favoriteColor;
}

To tell Room about the Person class, you annotate the class with @Entity and the primary key with @PrimaryKey:

@Entity
public class Person {
    @PrimaryKey String name;
    int age;
    String favoriteColor;
}

With those two annotations, Room now knows how to make a table to store instances of Person. One thing to note when you’re setting up your models is that every field that’s stored in the database needs to either be public or have a getter and setter in the usual Java Beans style (e.g. getName() and setName(String name)).

Your Person class now has all the information Room needs to be able to create the tables, but you don’t have a way to actually add, query, or delete a Person’s data from the database.
That’s why you’ll have to make a data access object (DAO) next.
The DAO is going to give an interface into the database itself, and will take care of manipulating the stored Person data.

Here’s a simple DAO for the Person class from before:

@Dao
public interface PersonDao {
    
    // Adds a person to the database
    @Insert
    void insertAll(Person... people);
    
    // Removes a person from the database
    @Delete
    void delete(Person person);
    
    // Gets all people in the database
    @Query("SELECT * FROM person")
    List<Person> getAllPeople();
    
    // Gets all people in the database with a favorite color
    @Query("SELECT * FROM person WHERE favoriteColor LIKE :color")
    List<Person> getAllPeopleWithFavoriteColor(String color);
    
}

The first thing to notice is that PersonDao is an interface, not a class.
This interface is straightforward enough that Room could write the class for you, so that’s exactly what it does!
The other interesting detail is the SQL statements in the @Query() annotations.
The SQL statements tell Room what information you want to get out of the database, and they can be as simple or as complicated as you want.
They’re also validated at compile-time, so you can have confidence that your syntax is correct without having to run your app to see if you get a runtime exception.
So if you change the method signature of List<Person> getAllPeopleWithFavoriteColor(String color) to List<Person> getAllPeopleWithFavoriteColor(int color), Room will give you a compile error:

incompatible types: int cannot be converted to String

And if you make a typo in the SQL statement, like writing favoriteColors (plural) instead of favoriteColor (singular), Room will also give you a compiler error:

There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: favoriteColors)

You may notice that you can’t get an instance of the PersonDao because it’s an interface.
To be able to use your DAO classes, you’ll need to make a Database class.
Behind the scenes, this class will be responsible for maintaining the database itself and providing instances of the DAOs you declared before.

You can create your database class with just a couple of lines of code:

@Database(entities = {Person.class /*, AnotherEntityType.class, AThirdEntityType.class */}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract PersonDao getPersonDao();
}

This describes the structure of the database, but the database itself will live in a single file.
To get an AppDatabase instance saved in a file named populus-database, you’d write:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "populus-database").build();

If you wanted to get all of the Person data that’s in the database, you could then write:

List<Person> everyone = db.getPersonDao().getAllPeople();

Keep in mind that RoomDatabase instances are expensive, so you’ll want to create just one and use it everywhere.
Dependency injection makes it easy to manage this instance.

Benefits of Using Room

Unlike most ORMs, Room uses an annotation processor to perform all of its data persistence magic.
This means that neither your application classes nor model classes need to extend from anything in Room, unlike many other ORMs including Realm and SugarORM.
As you saw when making mistakes with the @Query() annotations above, you also get compile-time validation of your database schema and SQL query statements, which can save you a lot of hassle compared to the runtime exceptions you’d get if you were using the native SQLite interface.

Room also allows you to observe changes to the data by integrating with both the Architecture Components’ LiveData API as well as RxJava 2.
This means that if you have a complicated schema where changes in the database need to appear in multiple places of your app, Room makes receiving change notifications seamless.
This powerful addition can be enabled with a one-line change in your DAO.
All that you have to do is change your @Query methods to return a LiveData or Observable object.

For example, this method:

@Query("SELECT * FROM person")
List<Person> getAllPeople();

becomes this:

@Query("SELECT * FROM person")
LiveData<List<Person>> /* or Observable<List<Person>> */ getAllPeople();

Room’s Biggest Limitation: Relationships

The biggest limitation of Room is that it will not handle relationships to other entity types for you automatically like some other ORMs will.
That means if you want to keep track of people’s pets like this:

@Entity
public class Person {
    @PrimaryKey String name;
    int age;
    String favoriteColor;
    List<Pet> pets;
}

@Entity
public class Pet {
    @PrimaryKey String name;
    String breed;
}

Then Room will give a compiler error since it doesn’t know how to store the relationship between a Person and its Pets:

Cannot figure out how to save this field into database. You can consider adding a type converter for it.

The compiler error suggests a type converter, which converts objects into primitives that can be directly stored in SQL.
Since List cannot be reduced into a primitive, you’ll need to do something different.
This is a one-to-many relationship, where one Person can have many Pets.
Room can’t model relationships like this, but it can handle the reverse relationship – each Pet has a single owner.
To model this, remove the pets field in Person, and add an ownerId field to the Pet class as shown:

@Entity
public class Person {
    @PrimaryKey String name;
    int age;
    String favoriteColor;
}

@Entity(foreignKeys = @ForeignKey(
            entity = Person.class,
            parentColumns = "name",
            childColumns = "ownerId"))
public class Pet {
    @PrimaryKey String name;
    String breed;
    String ownerId; // this ID points to a Person
}

This will cause Room to enforce a foreign key constraint between the entities.
Room won’t infer one-to-many and many-to-one relationships, but it gives you tools to express these relationships, fulfilling its promise as a high-level interface to SQLite.

To get all of the pets owned by a specific person, you can use a query that finds all pets with a given owner ID.
For example, you can add the following method to your DAO:

@Query("SELECT * FROM pet WHERE ownderId IS :ownerId")
List<Pet> getPetsForOwner(String ownerId);

If you want the pets field to stay in the Person model object, then there’s a few more steps.
The first is to tell Room to ignore the pets field – otherwise you’ll get the previously mentioned compiler error again.
You can do that by annotating the pets field with the @Ignore annotation like this:

@Entity
public class Person {
    @PrimaryKey String name;
    int age;
    String favoriteColor;
    @Ignore List<Pet> pets;
}

This tells Room not to read or write that field to and from the database.
This means that every time you get a Person from the DAO, the pets field will be set to null, so you’ll have to populate it yourself.
One way to do this is to have your Repository query for all Pets that a Person owns and set the pets field before passing a Person to the rest of your application.

Should You Use Room?

If you’ve already set up data persistence in your app and are happy with it, then you should keep it.
Every ORM and the native SQLite implementation are going to keep working just as they have before – Room is just another data persistence option.

If you’ve been using SQLite or are considering using it, though, you should absolutely try Room.
It has all the power you need to perform advanced queries while removing the need for you to write SQL statements to maintain the database yourself.

Alpha 3 of Room is available right now, but it’s very likely that the APIs will change before the final release, so you may want to avoid using it in a production app until then.
It’ll be available with the rest of the Architecture Components when they go stable in the coming months.

Avatar

Andrew Bailey

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project