Accessing Database with Room on Android

Photo by Sidekix Media on Unsplash
Photo by Sidekix Media on Unsplash
This article will introduce how to use Room to access databases in Android.

This article will introduce how to use Room to access databases on Android.

The complete code for this chapter can be found in .

Room

Room is a Database Object Mapping library to access databases. It makes the whole process of accessing databases very simple, and it also integrates Coroutine suspend function. In this article, we’ll build a simple app to show how to use Room.

First, create a new project. Then, in the project, add Room’s dependecies, as follows.

plugins {
    id 'kotlin-kapt'
}

dependencies {
    def room_version = "2.4.2"
    implementation "androidx.room:room-ktx:$room_version"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

@Entity

Each class that declares @Entity represents a table. In the following code, we declare class Employee and add @Entity on it. In @Entity, we specify the name of the table as employees.

In this table, we declare 4 columns. We can use @ColumnInfo to specify the name of each column. If not specified, it uses the field name by default. Finally, we use @PrimaryKey to specify the primary key.

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*

@Entity(tableName = "employees")
data class Employee(
    val name: String,
    val type: Type,
    @ColumnInfo(name = "created_at") val createdAt: Date = Date(),
    @PrimaryKey(autoGenerate = true) var id: Int = 0,
) {
    enum class Type {
        FULL_TIME, PART_TIME,
    }
}

@Dao

@Dao marks an interface or abstract class as Data Access Object (DAO). Data Access Object mainly defines the methods for accessing the database. As in the following code, we declare an interface EmployeeDao. Each method can be declared as a suspend function or a general method. Allows us to easily integrate with Coroutine.

CRUD:@Insert、@Update、@Delete、@Query

@Insert marks a method as an insert method. onConflict specifies the action to be taken when a conflict occurs. You can refer to all the actions in OnConflictStrategy.

@Update marks a method as an update method.

@Upsert marks a method as an insert or update method. It checks if the parameter already exists in the database. If it already exists in the database, it will update the entity.

@Delete marks a method as a delete method.

@Query marks a method as a query method. In @Query, we must write SQL statement and bind arguments. In @Query, not only SELECT, but also DELETE can be written. In addition, the returned value can also be Flow.

import androidx.room.*

@Dao
interface EmployeeDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(employee: Employee)

    @Update
    suspend fun update(employee: Employee)

    @Delete
    suspend fun delete(employee: Employee)

    @Query("SELECT * FROM employees")
    suspend fun findAll(): List<Employee>

    @Query("SELECT * FROM employees WHERE name = :name")
    suspend fun findByName(name: String): List<Employee>
}

@Transaction

@Transaction marks a method as a transaction method. In the transaction method, all operations are performed in a transaction.

import androidx.room.*

@Dao
abstract class EmployeeDao {
    @Delete
    abstract suspend fun delete(employee: Employee)

    @Transaction
    suspend fun delete(list: List<Employee>) {
        list.forEach { delete(it) }
    }
}

@RawQuery

@RawQuery marks a method that can execute the incoming SQL string. When we need to generate different SQL query strings according to different situations, we must generate SQL query strings by ourselves. Then, call the method marked with @RawQuery to execute our SQL query string.

import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery

@Dao
abstract class EmployeeDao {
    @RawQuery
    abstract suspend fun execSelect(query: SimpleSQLiteQuery): List<Employee>

    suspend fun findByNameOptional(name: String?): List<Employee> {
        var sql = "SELECT * FROM employees"
        name?.let {
            sql += " WHERE name = $it"
        }

        val query = SimpleSQLiteQuery(sql)
        return execSelect(query)
    }
}

@Database

@Database marks a class as RoomDatabase. This class must be an abstract class and inherit RoomDatabase, as EmployeeDatabase shown in the code below. In @Database, we use entities to specify the table in this database.

In EmployeeDatabase, we declare an abstract method called dao(). We don’t need to implement dao() as it will be generated automatically. After that, we’ll call this dao() to get EmployeeDao, and then execute the insert() or findAll() methods.

Then, we also declare a method called getInstance() to initialize an EmployeeDatabase object. When we call any method inside EmployeeDao, it must happen in a non-UI thread. Otherwise, an exception will be thrown. However, if you want to call any method inside EmployeeDao, you need to call .allowMainThreadQueries() when create EmployeeDatabase .

Additionally, we declare getTestingInstance() method. The difference from getInstance() is that it initializes an in-memory database, not a database file. This is quite handy during unit testing, since we don’t need to clean up the database files at the end.

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

@Database(entities = [Employee::class], version = 1)
@TypeConverters(Converters::class)
abstract class EmployeeDatabase : RoomDatabase() {
    abstract fun dao(): EmployeeDao

    companion object {
        @Volatile
        private var INSTANCE: EmployeeDatabase? = null

        fun getInstance(context: Context, path: String): EmployeeDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    EmployeeDatabase::class.java,
                    path,
                )
//                  .allowMainThreadQueries()
                    .build()
                INSTANCE = instance
                instance
            }
        }

        @VisibleForTesting
        @Synchronized
        fun getTestingInstance(context: Context): EmployeeDatabase {
            return Room
                .inMemoryDatabaseBuilder(context.applicationContext, EmployeeDatabase::class.java)
                .build()
        }
    }
}

@TypeConverters

In the above code, there is an annotation called @TypeConverters. When Room is about to write Employee to the database, or read data from the database and convert it to Employee, Room only knows how to convert primitive types. Let’s recall class Employee, its type and createdAt are both non-primitive types. So, we have to tell Room how to convert these non-primitive type data into primitive type.

@Entity(tableName = "employees")
data class Employee(
    val name: String,
    val type: Type,
    @ColumnInfo(name = "created_at") val createdAt: Date = Date(),
    @PrimaryKey(autoGenerate = true) var id: Int = 0,
) {
    enum class Type {
        FULL_TIME, PART_TIME,
    }
}

We declare 4 methods in class Converters. fromDate() is to tell Room how to convert Date to a primitive type. Here we choose to convert Date to Long, which is Date’s timestamp. On the other hand, toDate() is to tell Room how to convert Long to Date. Finally, fromType() and toType() are to tell Room how to convert between enum Employee.Type and String.

Annotation @TypeConverter should be added to each method. Then, specify class Converters for EmployeeDatabase with @TypeConverters.

import androidx.room.TypeConverter
import java.util.*

class Converters {
    @TypeConverter
    fun fromDate(value: Date) = value.time

    @TypeConverter
    fun toDate(value: Long) = Date(value)

    @TypeConverter
    fun fromType(value: Employee.Type) = value.name

    @TypeConverter
    fun toType(value: String) = Employee.Type.valueOf(value)
}

Debugging

When we are debugging, we may want to know the SQL statement that was executed. We can output the SQL statement executed each time with setQueryCallback().

@Database(entities = [Employee::class], version = 1)
@TypeConverters(Converters::class)
abstract class EmployeeDatabase : RoomDatabase() {
    abstract fun dao(): EmployeeDao

    companion object {
        fun getInstance(context: Context, path: String): EmployeeDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    EmployeeDatabase::class.java,
                    path,
                )
                    .setQueryCallback({ sqlQuery, bindArgs ->
                        println("SQL: $sqlQuery; Args: $bindArgs")
                    }, Executors.newSingleThreadExecutor())
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Example

In this article, we implement a simple insertion and query app, you can download the complete code. The following only shows EmployeeListViewModel to demonstrate how to access a database in a ViewModel.

import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class EmployeeListViewModel : ViewModel() {
    val employees: MutableLiveData<List<Employee>> = MutableLiveData()

    private lateinit var database: EmployeeDatabase

    fun initDatabase(context: Context) {
        val path = context.getDatabasePath("employee")
        database = EmployeeDatabase.getInstance(context, path.absolutePath)
    }

    suspend fun addEmployee(name: String, type: Employee.Type) = withContext(Dispatchers.IO) {
        val employee = Employee(name, type)
        database.dao().insert(employee)

        val list = database.dao().findAll()
        employees.postValue(list)
    }

    suspend fun searchByName(name: String) = withContext(Dispatchers.IO) {
        val list = if (name.isEmpty()) database.dao().findAll() else database.dao().findByName(name)
        employees.postValue(list)
    }
}

If you download this example and execute it, you can find the database file with Device File Explorer. Its path is /data/data/com.waynestalk.example/databases/, where there are 3 files, employee, employee-shm, and employee-wal. For each database, Room generates 3 files.

Conclusion

Room makes it easy to access databases, compared to ORMLite. Moreover, it also integrates suspend function.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like