Saving data using Room in Android

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 insertMultipleRecord(Movie... movies);

    @Insert
    void insertMultipleListRecord(List<Movie> movies);

    @Insert
    long insertOnlySingleRecord(Movie movie);

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

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

    @Update
    void updateRecord(Movie movie);

    @Delete
    void deleteRecord(Movie movie);

    @Delete
    void deleteAll(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().insertOnlySingleRecord(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.insertOnlySingleRecord(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().insertOnlySingleRecord(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().insertOnlySingleRecord(movie2);
        movie2.setId(movieId2);

        showMovies();

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

        showMovies();

        // delete movie
        db.movieDao().deleteRecord(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.

comments powered by Disqus