Saving data using Room in Android Android 15.11.2017

Saving data using Room in Android

Room is persistence library introduced at Google I/O 2017 which provides an abstraction layer over the SQLite database which in turn provides enhanced security, easy access, easy to setup and quick to get started with new database.

Let's look at major problems with SQLite:

  • There is no compile-time verification of raw SQL queries. For example if you write a SQL query with a wrong column name that does not exist in real database then it will give exception during run time and you can not capture this issue during compile time.
  • As your schema 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 is an annotation processing based SQL object mapping library provided by Google. Room is designed to abstract away the underlying database tables and queries. Therefore it provides an easy way to create and use Sqlite database. It is based on best-practices for persisting data in databases. For example, Room does, by default, not allow database access in the main thread. It also offers compile time check for the table name, column name and live monitoring for database changes using LiveData.

Room does not support object references between entities, to avoid potential performances issues. Even though you cannot use direct relationships, it is possible to define foreign key constraints between entities.

Add the following repository to your gradle (project) file.

allprojects {
    repositories {
        ...
        maven { url 'https://maven.google.com' }
    }
}

Add the following library and annotation processor to your gradle (app) file.

compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'

compile "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

The reason why annotation processor is needed is because all operations like Insert, Delete, Update etc are annotated.

Components of Room:

  • Entity. It’s nothing but a model class annotated with @Entity where all the variable will becomes column name for the table and name of the model class becomes name of the table.
  • Database. This is an abstract class where you define all the entities that means all the tables that you want to create for that database.
  • Dao. This is an interface which acts is an intermediary between the user and the database. All the operation to be performed on a table has to be defined here.

These components, along with their relationships with the rest of the app, appear in following figure

android_room_architecture.png

Entity

First of all let’s create our model class where all it’s variable name will become the column name and name of model class will become the table name.

@Entity
public class Movie {
    @PrimaryKey(autoGenerate = true)
    private long id;

    private String title;
    private int year;

    @Embedded(prefix = "rating")
    Rating rating;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public Rating getRating() {
        return rating;
    }

    public void setRating(Rating rating) {
        this.rating = rating;
    }

    @Override
    public String toString() {
        return "Movie{" +
                "title='" + title + '\'' +
                ", year=" + year +
                '}';
    }
}

public class Rating {
    private int id;
    private float Imdb;
    private float Tmdb;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public float getImdb() {
        return Imdb;
    }

    public void setImdb(float imdb) {
        Imdb = imdb;
    }

    public float getTmdb() {
        return Tmdb;
    }

    public void setTmdb(float tmdb) {
        Tmdb = tmdb;
    }
}

The table name will be Movie. The column names will be id, title, year, ratingImdb, ratingTmdb.

Room provides several annotations to define entities.

  • PrimaryKey marks a field as primary key field.
  • ForeignKey sets foreign key constraint.
  • Ignore ignores marked field, won’t be part of table.
  • ColumnInfo specifies column name in database, instead of using field name as column name.
  • Index creates index to speed up queries.
  • Embedded marks a field as embedded that makes all subfields of the embedded class as columns of entity.
  • Relation specifies relations, useful to fetch related entities.

Variables defined inside model class becomes the column name by default but there are few operation that can be performed.

  • Use column_name annotation to change the name for the column if you don’t want to use your variable name as a column name.
  • Your entity must have at least one primary key where if you want to auto increase the value than just add autogenerate = true while defining primary key, your data type should be of type int or long. Even when there is only 1 field, you still need to annotate the field with the @PrimaryKey annotation.
  • If you have object of some other model class where that also contains same variable name than just annotate that object with @Embedded(prefix="some_name"). This will add your prefix to all the variable name for that class.

  • If you want your data to get stored in some other format than that of the type of variable you defined in your model class than create another class add your conversion logic there and annotate those methods with @TypeConverter than add that class to your database class using annotation @TypeConverters.

Each entity must define at least 1 field as a primary key. 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 = {"title", "year"})
class Movie {
    public String title;
    public int year;

    @Ignore
    Bitmap poster;
}

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 = "movies")
class Movie {
    ...
}

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(value = {"title", "year"})})

class Movie {
    @PrimaryKey
    public int id;

    public String title;
    public int year;

    @ColumnInfo(name = "origin")
    public String country;

    @Ignore
    Bitmap poster;
}

Dao

To access your app's data using the Room persistence library, you work with data access objects, or DAOs. This set of Dao objects forms the main component of Room, as each DAO includes methods that offer abstract access to your app's database.

There are four annotations @Query, @Insert, @Update, @Delete to perform CRUD operations. @Query annotation is used to perform read operation on database.

@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 MovieDao {
    @Query("SELECT * FROM Movie")
    public User[] loadAllMovies();
}

Most of the time, you need to pass parameters into queries to perform filtering operations, such as displaying only movies who are older than a certain year. To accomplish this task, use method parameters in your Room annotation, as shown in the following code snippet:

@Dao
public interface MovieDao {
    @Query("SELECT * FROM Movie WHERE year > :year")
    public User[] loadAllMoviesOlderThan(int year);
}

Now let’s create our Dao interface which contains the list of operation that we would like to perform on table.

@Dao
public interface MovieDao {
    @Insert
    void insert(Movie... movies);

    @Query("SELECT COUNT(*) FROM Movie")
    int count();

    @Query("SELECT * FROM Movie")
    List<Movie> fetchAllData();

    @Query("SELECT * FROM Movie WHERE id =:id")
    Movie getSingleRecord(int id);

    @Update
    void update(Movie... movies);

    @Delete
    void delete(Movie... movies);

    @Query("DELETE FROM Movie")
    public void clear();
}

Room does not allow accessing the database on the main thread unless you called allowMainThreadQueries() on the builder because it might potentially lock the UI for a long periods of time. Asynchronous queries (queries that return LiveData or RxJava Flowable) are exempt from this rule since they asynchronously run the query on a background thread when needed.

You can run room on background thread using AsyncTask. This way of running room gives you an opportunity to perform tasks on main thread after room dao background operation is complete.

private void insertMovie() {
      new AsyncTask<Void, Void, Void>() {
          @Override
          protected Void doInBackground(Void... voids) {
              db.movieDao().insert(movie4);
              return null;
          }
        }.execute();
    }

private void loadMovies() {
    new AsyncTask<Void, Void, List<Movie>>() {
        @Override
        protected List doInBackground(Void... params) {
            return db.movieDao().fetchAllData();
        }

        @Override
        protected void onPostExecute(List items) {
            adapter.setItems(items);
        }
    }.execute();
}

Below codes show how to create executor and use it to call DAO methods on background thread. But this way of calling room on background thread can be used only if you don’t need to perform any operation on main thread after database operation is complete.

final Executor executor = Executors.newFixedThreadPool(2);
MovieDao dao = db.movieDao();

public void addMovie(Movie m){
    executor.execute(() -> {
        dao.insert(m);
    });
}

Database

Create a database holder called HollywoodDatabase extends RoomDatabase, we will define list of entities and database version. Class is annotated with @Database annotation. It is good practice to use singleton approach for the database, so you need to create an static method which will return instance of HollywoodDatabase.

@Database(entities = {Movie.class}, version = 1)
public abstract class HollywoodDatabase extends RoomDatabase {
    private static HollywoodDatabase db;

    public abstract MovieDao movieDao();

    public static HollywoodDatabase getAppDatabase(Context context) {
        if (db == null) {
            db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
                            // don't do this on a real app!
                            .allowMainThreadQueries()
                            .build();
        }
        return db;
    }

    public static void destroyInstance() {
        db = null;
    }
}

Integration

Now we are all set to perform operation on our database. So let’s add some records to the table

public class MainActivity extends AppCompatActivity {
    private String TAG = MainActivity.class.getSimpleName();

    HollywoodDatabase db;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        db = HollywoodDatabase.getAppDatabase(this);

        // add movie

        Movie movie1 = new Movie();
        movie1.setTitle("The Godfather");
        movie1.setYear(1972);
        Rating rating1 = new Rating();
        rating1.setImdb(9.2f);
        rating1.setTmdb(8.5f);
        movie1.setRating(rating1);
        db.movieDao().insert(movie1);

        Movie movie2 = new Movie();
        movie2.setTitle("The Dark Knight");
        movie2.setYear(2008);
        Rating rating2 = new Rating();
        rating2.setImdb(9.0f);
        rating2.setTmdb(8.3f);
        movie2.setRating(rating2);
        long movieId2 = db.movieDao().insert(movie2);
        movie2.setId(movieId2);

        showMovies();

        // update movie
        movie2.setTitle("The Godfather I");
        db.movieDao().update(movie2);

        showMovies();

        // delete movie
        db.movieDao().delete(movie2);

        showMovies();
    }

    private void showMovies() {
        List<Movie> movies = db.movieDao().fetchAllData();
        for (Movie movie : movies) {
            Log.d(TAG, "Movie: " + movie);
        }
    }

    @Override
    protected void onDestroy() {
        HollywoodDatabase.destroyInstance();
        super.onDestroy();
    }
}

Define relationships between objects

Because SQLite is a relational database, you can specify relationships between objects. Even though most object-relational mapping libraries allow entity objects to reference each other, Room explicitly forbids this. To learn about the technical reasoning behind this decision, see Understand why Room doesn't allow object references.

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 Artist, you can define its relationship to the Movie entity using the @ForeignKey annotation, as shown in the following code snippet:

@Entity(foreignKeys = @ForeignKey(entity = Movie.class,
    parentColumns = "id",
    childColumns = "movie_id"))
class Artist {
    @PrimaryKey
    public int id;

    public String name;

    @ColumnInfo(name = "movie_id")
    public int movieId;
}

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.

Type converters

So far, all of our fields have been basic primitives (int, float, etc.) or String. There is a good reason for that: those are all that Room understands "out of the box". Everything else requires some amount of assistance on our part (Date, Location, etc).

Type converters are a pair of methods, annotated with @TypeConverter, that map the type for a single database column to a type for a Java field. So, for example, we can:

  • Map a Date field to a Long, which can go in a SQLite INTEGER column
  • Map a Location field to a String, which can go in a SQLite TEXT column
  • Map a collection of String values to a single String (e.g., comma-separated values), which can go in a SQLite TEXT column
  • And so forth

However, type converters offer only a 1:1 conversion: a single Java field to and from a single SQLite column. If you have a single Java field that should map to several SQLite columns, the @Embedded approach can handle that

First, define a Java class somewhere.

@Entity(tableName = "movies")
class Movie {
    ...
}

Next, for each type to be converted, create two public static methods that convert from one type to the other. So for example, you would have one public static method that takes a Date and returns a Long (e.g., returning the milliseconds since the Unix epoch value), and a counterpart method that takes a Long and returns a Date. If the converter method is passed null, the proper result is null.

Then, each of those methods get the @TypeConverter annotation. The method names do not matter, so pick a convention that works for you.

Finally, you add a @TypeConverters annotation, listing this and any other type converter classes, to... something. What the "something" is controls the scope of where that type converter can be used.

A typical way of storing a date/time value in a database is to use the number of milliseconds since the Unix epoch (i.e., the number of milliseconds since midnight, 1 January 1970). Date has a getTime() method that returns this value.

So, let's define DataTransformer class that contains four methods, each annotated with @TypeConverter, for converting Date to and from a Long and Location to from String:

public class DataTransformer {
    @TypeConverter
    public static Long fromDate(Date date) {
        if (date == null)
            return(null);

        return(date.getTime());
    }

    @TypeConverter
    public static Date toDate(Long millisSinceEpoch) {
        if (millisSinceEpoch == null)
          return(null);

        return(new Date(millisSinceEpoch));
    }

    @TypeConverter
    public static String fromLocation(Location location) {
        if (location==null)
            return(null);

        return(String.format(Locale.US, "%f,%f", location.getLatitude(),
            location.getLongitude()));
    }

    @TypeConverter
    public static Location toLocation(String latlon) {
        if (latlon==null)
          return(null);

        String[] pieces=latlon.split(",");
        Location result=new Location("");

        result.setLatitude(Double.parseDouble(pieces[0]));
        result.setLongitude(Double.parseDouble(pieces[1]));

        return(result);
    }
}

HollywoodDatabase then has the @TypeConverters annotation, listing DataTransformer as the one class that has type conversion methods:

@Database(entities = {Movie.class}, version = 1)
@TypeConverters({DataTransformer.class})
public abstract class HollywoodDatabase extends RoomDatabase {
    private static HollywoodDatabase db;

    public abstract MovieDao movieDao();

    public static HollywoodDatabase get(Context context, boolean isMemory) {
        if (db == null) {
            if (isMemory) {
                db = Room.inMemoryDatabaseBuilder(context.getApplicationContext(),
                    HollywoodDatabase.class);
            } else {
                db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
                    // don't do this on a real app!
                    .allowMainThreadQueries()
                    .build();
            }
        }
        return db;
    }

    public static void destroyInstance() {
        db = null;
    }
}

Now, classes like Movie can use Date fields, which will be stored in INTEGER columns in the database.

Pre-populate Room database

Sometimes is very helpful have a pre-loaded data to start to use an application that uses a database as storing data. Room has a way to pre-populate the database at first run. The trick is override the onCreate method in Room callback.

db = Room.databaseBuilder(context.getApplicationContext(),
        HollywoodDatabase.class,
        "movie-db")
        .addCallback(new Callback() {
            @Override
            public void onCreate(@NonNull SupportSQLiteDatabase db) {
                super.onCreate(db);
                Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        List<Movie> items = new ArrayList<>();
                        Movie movie1 = new Movie();
                        movie1.setTitle("The Godfather");
                        ...
                        items.add(movie1);
                        Movie movie2 = new Movie();
                        movie2.setTitle("The Dark Knight");
                        ...
                        items.add(movie2);
                        getAppDatabase(context).movieDao().insert(items);
                    }
                });
            }
        })
        .build();

Room Database Migration

It is very common in any application that database tables are altered to introduce new features in applications. If your app uses Room and database tables need to be altered, then you should use migration process to retain data and make application continue to work.

Here’s what happens when you access the database for the first time:

  • The Room database is built
  • SQLiteOpenHelper.onUpgrade method is called and Room triggers the migrations
  • The database is opened

Typically, migration is an anonymous inner classes that handle the conversion from some older schema to a newer one. You pass a bunch of Migration instances to Room, representing different pair-wise schema upgrade paths. Room then determines which one(s) need to be used at any point in time, to update the schema from whatever it was to whatever it needs to be.

A Migration itself has only one required method: migrate(). You are given a SupportSQLiteDatabase, which resembles a SQLiteDatabase and allows you to execute SQL statements to modify the schema as needed.

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Movie ADD COLUMN comments INTEGER default 0 NOT NULL");
    }
};

The Migration constructor takes two parameters: the old schema version number and the new schema version number. Hence, the recommended pattern is to use anonymous inner classes, where you can provide the migrate() method to use for migrating the schema between that particular pair of schema versions.

Simply creating a Migration as a static field somewhere is necessary but not sufficient to have Room know about performing the migration. Instead, you need to use the addMigrations() method on RoomDatabase.Builder to teach Room about your Migration objects. addMigrations() accepts a varargs, and so you can pass in one or several Migration objects as needed.

@Database(entities = {Movie.class}, version = 2)
public abstract class HollywoodDatabase extends RoomDatabase {
    private static HollywoodDatabase db;
    ...
    public static HollywoodDatabase getAppDatabase(Context context) {
        if (db == null) {
            db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
                    .allowMainThreadQueries()
                    .addMigrations(MIGRATION_1_2)
                    .build();
        }
        return db;
    }
    ...
}

If you don’t want to provide migrations and you specifically want your database to be cleared when you upgrade the version, call fallbackToDestructiveMigration in the database builder:

db = Room.databaseBuilder(context.getApplicationContext(), HollywoodDatabase.class, "movie-db")
        .allowMainThreadQueries()
        .fallbackToDestructiveMigration()
        .build();

Kotlin && Room && RxJava

Add Room dependencies in module build.gradle:

apply plugin: 'kotlin-kapt'

...

dependencies {
    ...

    // for room
    implementation "android.arch.persistence.room:runtime:1.0.0"
    implementation "android.arch.persistence.room:rxjava2:1.0.0"
    kapt "android.arch.persistence.room:compiler:1.0.0"

    // for rxJava
    implementation "io.reactivex.rxjava2:rxjava:2.1.12"
    implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
}

Add Google’s Maven Repo in the project build.gradle:

allprojects {
    repositories {
        ...
        google()
    }
}

Create an Entity data class, for this, we need to annotate it with @Entity. 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.

@Entity
data class Movie(
    @PrimaryKey(autoGenerate = true)
    val id: Long,
    val name: String,
    val year: Int
)

Create a DAO using an interface and the @Dao annotation :

@Dao
interface MovieDao {
    @Query("SELECT * FROM Movie")
    fun getAllItems(): Flowable<List<Movie>>

    @Query("SELECT * FROM Movie")
    fun getAll(): Single<List<Movie>>

    @Query("SELECT COUNT(id) FROM Movie")
    fun count(): Single<Long>

    @Query("SELECT * FROM Movie WHERE id = :id")
    fun getItemById(id: String): Single<Movie>

    @Insert(onConflict = REPLACE)
    fun insert(movie: Movie)

    @Delete
    fun delete(movie: Movie)
}

We can use different types of observables.

Maybe<Movie>.

Here’s what happens:

  • When there is no movie in the database and the query returns no rows, Maybe will complete.
  • When there is a movie in the database, Maybe will trigger onSuccess and it will complete.
  • If the movie is updated after Maybe was completed, nothing happens.

Single<Movie>.

Here’s what happens:

  • When there is no movie in the database and the query returns no rows, Single will trigger onError(EmptyResultSetException.class).
  • When there is a movie in the database, Single will trigger onSuccess.
  • If the movie is updated after Single.onComplete was called, nothing happens, since the stream was completed.

Flowable<Movie>.

Here’s what happens:

  • When there is no movie in the database and the query returns no rows, the Flowable will not emit, neither onNext, nor onError.
  • When there is a movie in the database, the Flowable will trigger onNext.
  • Every time the movie data is updated, the Flowable object will emit automatically, allowing you to update the UI based on the latest data.

Create a Database holder that extends RoomDatabase. The class needs to be annotated with @Database annotation.

@Database(entities = arrayOf(Movie::class), version = 1)
abstract class MovieDatabase : RoomDatabase() {
    abstract fun movieDao(): MovieDao
}

Room Database instance is expensive, so we can create the AppDatabase object in the Application object and access it where need it or take the standard singleton approach.

class MyApp : Application() {

    companion object {
        var database: MovieDatabase? = null
    }

    override fun onCreate() {
        super.onCreate()
        MyApp.database =  Room.databaseBuilder(this, MovieDatabase::class.java, "movies.db").build()
    }
}

Room enforces database access to be off the main thread, since database operations can be potentially long-running. Using Rx we can easily move the database call to a background I/O thread.

To insert into the database, I’ve used an RxJava Single to run this action asynchronously:

fun addMovie(name: String, year: Int) {
    val item = Movie(0, name, year)

    Single.fromCallable {
        MyApp.database?.movieDao()?.insert(item)
    }.subscribeOn(Schedulers.io())
            .subscribe()
}

Using the dao method that returns a Flowable, we can also set up a subscriber to get updates to the database as they come in.

MyApp.database?.movieDao()?.getAllItems()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { items ->
        // do something with 'users'
    }

Following is my Activity.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // add items
        for (i in 1..5) {
            addMovie("Movie $i", 2000 + i)
        }

        // count items
        MyApp.database?.movieDao()?.count()
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(AndroidSchedulers.mainThread())
            ?.subscribe { count ->
                Log.d("TAG", "Total: $count")
            }

        // list all
        MyApp.database?.movieDao()?.getAll()
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(AndroidSchedulers.mainThread())
            ?.subscribe { items ->
                Log.d("TAG", items.toString())
            }

    }

    fun addMovie(name: String, year: Int) {
        val item = Movie(0, name, year)

        Single.fromCallable {
            MyApp.database?.movieDao()?.insert(item)
        }.subscribeOn(Schedulers.io())
                .subscribe()
    }
}