Advanced Usage of Room Database in Android Apps

Oussama Azizi
Android Development Hub
5 min readOct 4, 2023

Room is a powerful and flexible library in Android Jetpack that simplifies local data storage and management. While it’s relatively straightforward to set up a basic Room database, advanced usage of Room can take your Android app’s data handling capabilities to the next level.

1. Database Migrations

One common challenge in Android app development is handling database schema changes over time. When you need to make changes to your Room database schema, you must create a database migration. Room provides a straightforward way to define and execute these migrations using the @Database annotation and the Migration class.

@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() {

abstract fun userDao(): UserDao

companion object {
private const val DATABASE_NAME = "app_database"

val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// Define the necessary SQL statements for migration
database.execSQL("ALTER TABLE user ADD COLUMN email TEXT")
}
}

fun build(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_1_2)
.build()
}
}
}

In this example, we define a simple database migration that adds a new column to the User table. You can create more complex migrations as needed for your app's evolution.

2. Type Converters

Room database primarily supports common data types like strings, integers, and booleans. However, you might encounter scenarios where you need to store more complex data types in your database. Type converters allow you to define custom conversion logic between complex data types and supported data types.

@Entity
data class User(
@PrimaryKey val id: Long,
val name: String,
@TypeConverters(AddressConverter::class) val address: Address
)

class AddressConverter {
@TypeConverter
fun fromAddress(address: Address): String {
// Convert Address to a JSON string
return Gson().toJson(address)
}

@TypeConverter
fun toAddress(addressJson: String): Address {
// Convert JSON string back to Address
return Gson().fromJson(addressJson, Address::class.java)
}
}

In this example, we use a custom type converter to store the Address object as a JSON string in the database and convert it back to the Address object when retrieving data.

3. Complex Queries

Room supports simple CRUD (Create, Read, Update, Delete) operations out of the box, but you might need to perform more complex queries involving multiple tables or advanced filtering. Room provides powerful query capabilities using SQL-lite queries with LiveData or RxJava observables.

@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): LiveData<List<User>>

@Query("SELECT user.*, address.street FROM user JOIN address ON user.address_id = address.id")
fun getUsersWithStreet(): LiveData<List<UserWithStreet>>
}

In the first query, we select users older than a specified age, and in the second query, we perform a join operation to retrieve users along with their street addresses.

4. One-to-Many and Many-to-Many Relationships

Room makes it easy to model and query one-to-many and many-to-many relationships in your database. You can use annotations like @Relation, @ForeignKey, and @Embed to define and navigate these relationships.

@Entity
data class Library(
@PrimaryKey val libraryId: Long,
val libraryName: String
)

@Entity
data class Book(
@PrimaryKey val bookId: Long,
val bookTitle: String,
@ForeignKey(entity = Library::class, parentColumns = ["libraryId"], childColumns = ["libraryId"])
val libraryId: Long
)

data class LibraryWithBooks(
@Embedded val library: Library,
@Relation(
parentColumn = "libraryId",
entityColumn = "libraryId"
)
val books: List<Book>
)

In this example, we have two entities, Library and Book, with a one-to-many relationship. We use @Relation to define how the entities are related and create a LibraryWithBooks data class to represent the combined result.

5. Entity Inheritance

Room also supports entity inheritance, allowing you to create a hierarchy of entities that share common fields. You can use the @Entity annotation's inheritSuperIndices and indices properties to define entity inheritance.

@Entity(inheritSuperIndices = true)
open class Person(
@PrimaryKey val personId: Long,
val firstName: String,
val lastName: String
)

@Entity
data class Student(
@PrimaryKey val studentId: Long,
val grade: Int
) : Person(studentId, "John", "Doe")

In this example, we define a base Person entity with common fields and a derived Student entity. The Student entity inherits the primary key and fields from Person.

6. FallbackToDestructiveMigration

While database migrations are a powerful tool, there are situations where it’s not possible or practical to define migrations for all possible schema changes. In such cases, you can use fallbackToDestructiveMigration() to allow Room to recreate the database from scratch when a migration is not available.

@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DATABASE_NAME = "app_database"

fun build(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
}
}
}

Be cautious when using this approach, as it will result in data loss when the database is recreated.

7. Export Schema

Room provides an option to export the schema of your database as a JSON file. This can be useful for sharing database information with other developers or for documentation purposes.

@Database(entities = [User::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
// ...
}

After defining exportSchema = true in your @Database annotation, you can find the generated schema file in the "schemas" directory of your project.

8. Migrating from Legacy Databases

If your app previously used a different database solution and you want to migrate the data to Room, you can use the createFrom method to create a new Room database from the existing database.

Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.createFrom(existingDatabase)
.build()

This allows for a smooth transition from legacy databases to Room without losing existing data.

9. Room with Coroutines

If you prefer working with Kotlin Coroutines, Room provides native support for them as well. You can use the suspend modifier in your DAO methods to perform database operations asynchronously.

@Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAllUsers(): List<User>
}

This allows you to seamlessly integrate Room with Coroutines to create more concise and readable asynchronous code.

10. Database Pre-Population

If your app requires pre-populating the database with initial data, Room offers a callback mechanism for this purpose. You can create a class that implements RoomDatabase.Callback and override the onCreate method to perform the initial data insertion.

class AppDatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// Perform initial data insertion here
}
}

Then, you can set this callback when building your Room database:

Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addCallback(AppDatabaseCallback())
.build()

This is particularly useful for seeding your database with default values or initial configuration.

11. Room with Encrypted Data

Room can work seamlessly with encrypted data using Android’s Encrypted File API. You can encrypt your Room database files to enhance data security, especially for sensitive user information.

To set up encrypted Room databases, you can use libraries like SQLCipher, which provides encryption support for SQLite databases.

These advanced usage scenarios demonstrate Room’s versatility and adaptability to various app development needs. By mastering these features, you can efficiently manage complex data storage and retrieval requirements while ensuring data security and integrity in your Android apps.

--

--