Working with SQLite database in Android with the help of Room library

Working with SQLite database in Android with the help of Room library

Introduction

Room is a library recommended by Google as one of the components of the so-called. “Android Architecture Components” approach. Room is a wrapper around the SQLite database and is an abstraction layer that facilitates working with the database. The Room library takes over most of the responsibilities so that now we can create tables and manage data more easily.
Room has a so-called compile-time checks ie. checking the code during compilation and if there is an error, it will be shown during the compilation itself. In this way, Room saves us from irritating small errors that occur when working with the SQL database (lack of semicolons or spaces…) that cause RunTimeException.

In order to work with this library, it is necessary to define dependencies:

room and MVVM diagram

The Room library consists of three main components:

Entity (table)

Entity is a class that represents a database table and is marked with @Entity. In the continuation of this annotation (within the brackets) we can define a table name that can be different from the class name itself. If we don’t define the table name this way, Room will generate a table with the same name as the class name.

Every entity must have a constructor! A constructor is most often defined so that its parameters match the fields (based on type and name). However, the constructor doesn’t have to receive all the fields as parameters, the field we don’t want to put in the constructor must be marked with the @Ignore annotation, otherwise Android Studio will show an error. Also, within the constructor we don’t have to put a field that is autogenerated (ie autoGenerate = true), but for that field there must be a public setter (we won’t use it, but it is necessary for the Room library).

@PrimaryKey

When working with the Room library, one column must have a defined so-called PrimaryKey. Marking the column that will represent “Primary key” is done by adding the @PrimaryKey annotation to that field. When we define that a field is PrimaryKey then we can use its method autoGenerate() or simply add (autoGenerate = true) in brackets.

Example

In this example, the first field mId is automatically generated, so we will not include it in the constructor, but it is still necessary to provide its setter because it is needed by the Room library (for other fields, setters are not necessary).

NOTE:
The constructor can possibly be withoutno arguments but then there must be defined setters for all fields.

@ColumnInfo

Room by default generates a table with the same column name as the class field, however, if we want the column name to have a different name from the name of the field it represents, then we must use the @ColumnInfo annotation and define a different column name in brackets.

@Embedded

With this notation, it is possible to nest one table inside another (one-one reaction).

Now we can make queries in this table and the User object has all the columns: id, firstName, street, state, city, and post_code.

NOTE:
We can add a prefix to all column names of an embedded table if we define it in brackets with prefix:

@ForeignKey

With this annotation, we connect two entities (tables) by connecting their columns (the column from the child entity with the column value from the parent entity). This is practically an additional annotation within the @Entity annotation of the child entity:

Example

First we define the parent entity (table) Course.

The previous table can be connected to several students, so to define the child table Student we will use the annotation @ForeignKey:

In this example, we connected the id column values of the User table with the userId values of the Repo table. The meaning of the assigned values for onDelete/onUpdate are as follows:

  • int CASCADE – The “CASCADE” action propagates the delete or update operation of the parent key to each dependent child key.
  • int NO_ACTION – Default behavior when a parent key is modified or deleted from the database, and no other special action is taken.
  • int RESTRICT – The RESTRICT action means that the application is prohibited from deleting (for onDelete ()) or changing (for onUpdate ()) a parent key when there are one or more child keys mapped to it.
  • int SET_DEFAULT – “SET DEFAULT” actions are similar to SET_NULL, except that each of the key’s child columns is set to contain the column’s default value instead of NULL.

NOTE:
It is known that creating such a connection does not necessarily lead to a relationship between those tables, but only helps to clearly define what will happen to the “child entity” when a member of the “parent entity” is deleted (onDelete) or updated (onUpdate).

@Relations

With this annotation it is possible to connect two tables without using @Foreign.

Example

The previous example showed the connection of two tables with the help of the annotation @ForeignKey, we can solve the same request without the Foreign key using another notation called @Relation. For that, it is necessary to create a new class with which we can create an instance that contains both a parent entity instance and a list of child entity instances:

Later, the DAO is created as follows:

More on this in the next section.

DataAccess Object – DAO

DAO (data access object) as its name suggests: data access object. A DAO must be either an interface or an abstract class, ie. its methods have no body because Room will generate all non-walkable code depending on the annotation (@Insert, @Delete, @Query…). In this way, the amount of code that must be created by the programmer is reduced.
Another great advantage of this object is the ability to validate SQL statements during compilation (eng. “at compile-time”) and thus indicate errors in a timely manner, which was not possible when working with the SQLiteOpenHelper class.

@Dao

This annotation lets Room know that the given interface or abstract class is actually a DAO. In general, for each entity its own DAO is created, so for our entity from the example “BuyItem” the DAO would look like this:

@Insert

This annotation marks the method responsible for entering data into the database.

This is quite enough to replace the entire insertItemToDB() method that we used in the example from SQLiteOpenHelper classes:

@Delete

This annotation marks the method responsible for deleting data from the database.

This is quite enough to replace the entire removeItemFromDB() method we used in the example from SQLiteOpenHelper classes:

@Query

This annotation marks the method responsible for obtaining data from the database depending on the defined condition (query):

When writing this query, you can notice that Android studio indicates errors and offers solutions and changes. And these two lines are quite enough to replace the entire getAllItemsFromDB() method that we used in the example from the SQLiteOpenHelper class.

Passing parameter to query

It is often necessary to pass some parameter with which to filter the query, so it looks like in the following example:

Example

We can even pass multiple parameters as in the following example:

Example

We can also pass as parameters some collection (can return a LiveData object):

NOTE:
When you pass data through the layers of the application architecture (from the Room database, through the Repository class, then through the ViewModel class all the way to the user interface, i.e. the View class (activity or fragment), that data must be LiveData in all layers, or in other words, all data that Room sends from DAO through some query to the Repository, and then from the Repository to the ViewModel, must be LiveData. The explanation for this lies in the fact that we don’t need to set anywhere in the application that Room does it for us, so we don’t need the MutableLiveData object anywhere (which, unlike the LiveData object, has a publicsetter) methods).

Example

This is what a typical Dao interface looks like:

DataBase

The class representing the Room database must be abstract and extend the RoomDatabase class:

In addition to this, we need to mark this class so that Room knows which class it is, and this is achieved through the @Database annotation. In the continuation of this annotation (in parentheses), we define which entities (tables) this database contains, as well as the current version of the database:

Creating a database

When creating databases, it is “smart” to use the singleton pattern. The database is created using the Room method called databaseBuilder(). This method accepts parameters: context, “class that defines the base” and base name. In order to create and initialize the database, we need to call the build() method:

If we want to prevent database migration problems then it is good to call the method fallbackToDestructiveMigration().

NOTE:
If for debugging purposes you want to access the database (using DeviceFileExplore) and view it using DB Browser for SQLite or a similar application, you need to call the method setJournalMode(JournalMode.TRUNCATE), otherwise the database you are browsing will be empty.

In addition to this, it is necessary to create an abstract method that will return the corresponding Dao object:

Example

This is what a whole class looks like:

Repository

Although this class does not belong directly to the Room library but is hierarchically above it, it will still be covered in this article because the Room library does not allow operations on the base from the main threed! For this reason, all methods related to CRUD operations must be executed on the background thread, and the only exception is the method that returns the LiveData object, because Room takes care of it and automatically calls it from the background thread.

One way to execute a method on a background thread is for it to extend the AsyncTask class, however, since Android 11 (API 30) the AsyncTask class has been deprecated, so it is necessary to use another approach (see here how our Repository class would look like if we used AsyncTask methods). Since we need to avoid using an AsyncTask, another way to execute is to use an Executor object.

Executor service is created in the Database class by defining the necessary number of threads:

Now within our repository class we can use the ExecutorService and execute the DAO method asynchronously from the background thread.

Example

The entire example project can be found on Github under the name “sqliteWithRoomLib”.

×

Example

×

×

×

×

fallbackToDestructiveMigration()

This method is in charge of database migration when we change the database version and has a similar role as the OnUpgrade() method from the SQLiteOpenHelper class.

This method allows the Room library to destructively recreate database tables if no migrations are found to migrate old database schemas to the latest schema version. Read more about migration with Room library here.

×

Primary key

Primary key is a column that represents a unique identifier of a row in a database table. Each table must have at least one Primary key that cannot be NULL. Primary key is defined when creating the table as follows:

Another syntax for defining a Primary key looks like this: