Using Room Persistence in Android

in #utopian-io4 years ago (edited)

Repository

https://github.com/aosp-mirror/

What Will I Learn?

  • You will learn what Room Persistence is about
  • You will learn the benefits of adopting Room Persistence
  • You will learn how to add Room Persistence to your Android application
  • You will learn how to setup and use Room Persistence within your application
  • You will learn about Executors in android

Requirements

  • Android Studio 2.3 and above
  • Basic Knowledge of native Android development using Java

Difficulty

  • Intermediate/Advanced

Tutorial Contents


Image Source

In the previous tutorial, i explained the use of the android content provider as an abstraction layer over the existing SQLite APIs in android. In this tutorial, i will be introducing you to a more efficient abstraction layer for SQLite APIs: Room Persistence.

Room Persistence is a library that is added to an android application so as to effectively manage communication betwen the user of the application and the database of the application. Much like the content provider, it acts as a form of middle man that shields the SQLite storage framework from direct interaction with user in the app. This means it provides some form of abstraction as we really have no idea how it communicates with the database.

Benefits of Room Persistence

The content provider poses problems when used as the link between user and database, apart from tedious code writing, there is also the danger of runtime failure of application. The room persistence library deals with these problems and is thus very efficient for use in database management.

Some benefits of Room Persistence include:

  • It performs SQL query validation at compile time (unlike content provider and raw SQL which is at compile timeI)
  • Simplification of access to database, ensures less code complexity
  • It reduces boilerplate codes within the application
  • It takes care of schema generation and data manipulation in the code
  • Most importantly, it integrates well with other android architecture components such as LiveData and ViewModel.

Adding Room Persistence to application

Create a new project in android studio. The first step in adding room persistence to your android project is to add the corresponding dependencies/files necessary for its functionality. Add the following to your build.gradle file,

compile ‘android.arch.persistence.room:runtime:1.1.0’
annotationProcessor ‘android.arch.persistence.room:compiler:1.1.0’

For newer versions of gradle, you can use the implementation keyword instead of compile as shown below,

implementation ‘android.arch.persistence.room:runtime:1.1.0’
annotationProcessor ‘android.arch.persistence.room:compiler:1.1.0’

Note- Newer version numbers may have become available as at the time you read this tutorial

The first line with the implementation keyword adds the needed classes for room to function while the annotation processor is used to perform annotation calls in your project. Annotations are a vital part of room and therefore the annotation processor must be added.

Setting up Room Persistence Library in application

To properly understand how room works and how to implement its setup, three concepts must be explained, they are:

  • Entity
  • DAO (Data Access Object)
  • Room Database
Entity

Entity is a model class that represents a database table in the application. It is annotated with the @Entity annoation which helps create an SQLite table in the database using the model class. Each field variable in this class represents a column in the table. Getters and Setters can be used to retrieve the objects of this class in the same way as you would retrieve from a normal POJO class.

Shown below is a model class or database table representing Student details in a school. Create a new class in your project, i have named my class Students:

@Entity(tableName = "StudentsDB")
public class Students{

@PrimaryKey (autogenerate=true)
private int id;

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

private String class;

private int age;

private int testScore;

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

public int getId(){
return id;
}

public void setName(String name){
this.name = name;
}

public String getName(){
return name;
}

public void setClass(String class){
this.class =  class;
}

public String getClass(){
return class;
}

public void setAge(int age){
this.age = age;
}

public int getAge(){
return age;
}

public void setTestScore(int testScore){
this.testScore = testScore;
}

public int getTestScore(){
return testScore;
}


}

My class is annotated with the @Entity annotation and has a table name passed into it. If no table name is provided, room assumes the name of the class, in our case- Students, as the name of the table.

Each field in the class represents a column in the table that will be created by room. A column name can be specified
as done for the name field, using the @ColumnInfo annotation and passing in the name of the column as you prefer. If this annotation isn't used, Room uses the name of the field as the name of the column.

An id field is provided which represents a unique row in the table. It is annotated with the @PrimaryKey annotation to tell Room of its functionality and if the autogenerate parameter is set to true, Room automatically handles generation of the unique id for each row.

Getters and Setters are used to manipulate the fields and alternatively a constructor may be used instead of setters to instantiate the Students object

To disable creation of a column for a field in the entity class, the field is annotated with the @Ignore annotation as follows,

@Ignore
private boolean gender; // true for boys, false for girls

This causes the field gender to be ignored when creating the table, hence data of type gender cannot be stored in the room database.

DAO (Data Access Object)

The DAO is an interface that creates a data access object for room using the @DAO annotation. It uses declarative methods to access data which requires Room's annotations to identify operations carried out in the database. This interface has the main functions of : Getting entity classes, in other words database tables and also persists changes back to the database from the user. The DAO is where the logic for the CRUD operations carried out on the database are placed.

Create an interface class called MyDAO and type in the following code to create our DAO,

@DAO
public interface MyDAO{

@Query("SELECT * FROM StudentsDB")
List<Students> getAllStudents();

}

This interface shows a method called getAllStudents() and this method returns a list of Students objects. An annotation called @Query is used to annotate the method. This annotation in room represents a query or read operation on the database.

The parameters for the query are passed in as a string using the SQL syntax SELECT and FROM and also the name of the table, StudentsDB. This simply means select all student objects present in the StudentsDB table and return them as a list of students to the user. Do note that if we didnt give our entity a table name, we would have passed in the name of the class instead of the string "StudentsDB".

Note that the parameters passed in to the @QUERY annotation determine what part of the table is queried and what data will be returned. For example to query the table and return the rows where the test score of the students is 10 or maximum, we implement as shown below,

@Query("SELECT * FROM StudentsDB WHERE testScore LIKE :testScore")
List<Students> getBestStudents(int testScore);

The parameters passed simply mean, query the database with the table name "StudentsDB", where the testScore column equals the testScore passed into the method. If a 10 is passed into the method when it is called, room returns a list of students object with that score.

To insert a new student, we simply pass in the student object to be inserted as parameter to an appropriate object that has been annotated with the room annotation @Insert, the code snippet shown below illustrates this:

@Insert
void insertStudent(Students student);

Room automatically does the insertion into our table, creating a new row. Parameters can also be passed into the @Insert annotation to determine where in the table should be inserted or how it should be done.

The DAO can also perform an update operation on the database. It does this by using an @Update annotation, with details of where should be updated passed in as a parameter, code is shown below:

@Update("UPDATE Students WHERE name= :name")
void updateStudentName(String name);

This operation updates the table with a new set of name data that is passed into the method.

The last operation of CRUD (Delete) can be performed from the DAO using the @Delete annotation and passing in the Student object to be deleted, see code below:

 @Delete
void deleteStudent(Students student);
Room Database

The final part of setting up Room is done in a class that extends RoomDatabase. This class is an abstract class that has a method which returns a DAO object. The returned DAO object is then used to call any of the CRUD operations method that are present in the DAO interface.

The room database class is also supplied with the entity or entities as the case maybe, for which it should create tables for. The code snippet below shows this in action:

@Database(entities = Students.class, exportSchema = false, version = 1)
public abstract class StudentsDatabase extends RoomDatabase {
    public abstract MyDAO getDAO();
}

The getDAO() method returns the DAO object and the entity on which this DAO can be used on is the Students.class entity passed into the @Database annotation. Other parameters passed into the annotation are: the exportSchema, which determines if we want to export our database to other apps or if it can be used by other apps and the version number of the database, a very necessary parameter for upgrade of the database.

Type Converters

Sometimes, we want to store custom fields in our database that are not part of the basic database variables(Strings, integers, booleans etc). Such fields include objects of type Date, Bitmap etc. To do this, room has a special concept known as Type converters.

Screenshot (313).png
Image Source
Image showing data types that can be stored in SQL

Type converters help room to convert your field in any form it is, into a form that can be saved into the database. Lets try to save in Date of birth of Students into the database. In our entity class add a new field variable providing getters and setters,

@Entity
public class Students{

private Date dateOfBirth;

public Date getDateOfBirth(){
return dateOfBirth;
}

public void setDateOfBirth(Date dateOfBirth){
this.dateOfBirth = dateOfBirth;
}

}

Now create a class called DobConverter as follows,

public class DobConverter{
@TypeConverter
    public static Date convertToDate(Long value) {
        return value == null ? null : new Date(value);
    }

    @TypeConverter
    public static Long convertToLong(Date value) {
        return value == null ? null : value.getTime();
    }
}

Using the @TypeConverter annotation, we create two methods, one method converts from Date to a long object, the other converts from a long object to a date object. This type converter is then used by room to know the form in which to save the Date object in the entity class.

To use this type converter, we simply pass it into our room database class as an annotation parameter, code is shown below,

@Database(entities = Students.class, exportSchema = false, version = 1)
@TypeConverters{(DobConverter.class})
public abstract class StudentsDatabase extends RoomDatabase {
    public abstract MyDAO getDAO();
}

Note- More than one type converter can be used, simply pass them in as parameters seperated by commas into the @TypeConverters annotation

Using Room in our application

Now that we have set up our room database, lets make use of it within our application. In any activity or fragment or any class at all, simply create an instance of your room database class and build the database. This instance is then used to access the DAO and perform the required database CRUD operation, see code below to use room in an activity:

private StudentsDatabase studentDatabase;

@Override
protected void onCreate(Bundle onSaveInstanceState){
super(onSaveInstanceState);
studentDatabase = Room.databaseBuilder(getApplicationContext(), StudentsDatabase.class, "Students Database")
        .build();
}

Note- It is best practice to create a single instance of the Roomdatabase which can be injected anywhere in the app, you can use dependency injection for this, check out my tutorial on how to use dependency injection via dagger 2

This creates an instance of the database class using the StudentsDatabase class and a name for the database and then the instance is built using the build() method. To perform CRUD operations, this instance is used to call the DAO corresponding method and passing it the required parameter(s).

//queries all students in the database table
List<Students> allStudents = studentDatabase.getDAO.getAllStudents();

//queries best students in the database table
List<Students> bestStudents = studentDatabase.getDAO.getBestStudents();

//inserts a new student
studentDatabase.getDAO.insertStudent(new Student());

//inserts a new student
studentDatabase.getDAO.deleteStudent(student);

Note that these operations must be performed on a background thread using an asynctask or new thread. To do this with a thread, see code below:

 List<Students> allStudents;
new Thread(new Runnable() {
 @Override
 public void run() {
allStudents = studentDatabase.getDAO.getAllStudents();
 }
 }) .start();

This creates a new thread and performs the query operation on the room database.

Database Migration

As with other database models that have to be upgraded when a new column is added to the table, room is no exception and uses Migration to move from one version of the database to another when an upgrade has been carried out.

Take for example we add students unique registration number to the database entity,

private int registrationNumber;

public int getRegistrationNumber(){
return registrationName;
}

public void setRegistrationNumber(int regNumber){
this.registrationNumber =  regNumber;
}

In our StudentsDatabase class we have to increase the version number of the database and create a static final variable that describes the migration performed, see code below:

@Database(entities = Students.class, exportSchema = false, version = 2)
@TypeConverters{(DobConverter.class})
public abstract class StudentsDatabase extends RoomDatabase {

public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE StudentsDB"
                + " ADD COLUMN registrationNumber INTEGER");
    }
};

    public abstract MyDAO getDAO();
}

This string simply connotes that when database version number is 2, execute the migration by adding a column for registration number of integer type in the table with name StudentsDB.

Now to add this migration simply append it to the creation of the database instance in the onCreate() of our activity in the following way,

studentDatabase = Room.databaseBuilder(getApplicationContext(), StudentsDatabase.class, "Students Database")
        .addMigrations(StudentsDatabase.MIGRATION_1_2)
        .build();

This way room knows how to migrate when a changes are made to the database. It is worthy of mention that many migrations can be added to the build as required.

Executors

Remember when i mentioned that room operations must be called on a background thread and gave a code snippet on how to do this using a new thread creation, well it just so happens that a problem is posed by such an approach. Apart from creating a new thread every time, the changes cannot be updated on the UI. This is where Executors come in.

Executors are used to handle a submitted runnable task and can run them on the background thread more efficiently than a thread creation or asynctask would. Lets create a new class called AppExecutor, and add the following lines of code.

public class AppExecutor{

private static AppExecutor sInstance;
private Executor diskIo;
private Executor UIThread;

private AppExecutor(Executor diskIo, Executor UIThread){
this.diskIo =  diskIo;
this.UIThread = UIThread;
}

public static AppExecutor getInstance(){
if (sInstance == null){
sInstance = new AppExecutors(Executors.newSingleThreadExecutor(), new MainThreadExecutor());
}

public Executor getDiskIo(){
return diskIo;
}

public Executor getUIThread(){
return UIThread;
}

}

}

This class simply creates two executors, one for handling background code logic (diskIo) and the other for updating the UI (UIThread). The class is returned as a singleton and instantiated by passing in instances of the Executors using Executors.newSingleThreadExecutor() and new MainThreadExecutor(). These executors are then used to perform the background work for accessing room database in our activity or any class. See code below,


private List<Students> allStudents;
AppExecutor.getInstance().diskIo().execute(new Runnable(){
@Override
public void run(){

//perform code for database operation
allStudents = studentDatabase.getDAO.getAllStudents();

}
})

The AppExecutor instance calls the diskIo executor and executes it passing in a runnable that contains the code to be executed in the run() method. This way, calls to threads are more efficient as only one instance of the diskIo thread is called. This runs on a background thread meaning our room database can function effectively.

Supplementary Resource

To learn more about the usage of Room, checkout codes from applications i developed using room

https://github.com/demistry/MedManager/tree/master/app/src/main/java/com/medmanager/android/model/storage

https://github.com/demistry/CryptoCur_App/tree/master/app/src/main/java/com/android/cryptocurapp/storage

https://github.com/demistry/DemSoccer/tree/master/app/src/main/java/com/djtech/demsoccer/storage

Curriculum

Proof of Work Done

Here are links to applications i developed using room,

https://github.com/demistry/MedManager

https://github.com/demistry/CryptoCur_App

Sort:  

Thanks for the contribution!

Your contribution has been evaluated according to Utopian rules and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post,Click here


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Hey @davidemi
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!