Entities and DAOs
Room Architecture
Room is SQLite wrapped in a compile-time verified abstraction. It has three main components:
- Entity: A class that maps to a database table
- DAO (Data Access Object): An interface defining database operations
- Database: The holder for the database instance
Defining Entities
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val description: String,
val isCompleted: Boolean = false,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
@Entity marks the class as a database table. Each property maps to a column. Use @ColumnInfo to customize column names. @PrimaryKey(autoGenerate = true) creates an auto-incrementing ID.
Composite Primary Keys
@Entity(
tableName = "task_tags",
primaryKeys = ["taskId", "tagId"]
)
data class TaskTagCrossRef(
val taskId: Long,
val tagId: Long
)
Type Converters
Room only knows how to store primitives and Strings. For complex types, write a converter:
class DateConverter {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun toTimestamp(date: Date?): Long? = date?.time
}
Register converters on the database class:
@Database(entities = [Task::class], version = 1)
@TypeConverters(DateConverter::class)
abstract class TaskDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
Writing DAOs
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY created_at DESC")
fun getAllTasks(): Flow<List<Task>>
@Query("SELECT * FROM tasks WHERE id = :taskId")
fun getTaskById(taskId: Long): Flow<Task?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task): Long
@Update
suspend fun updateTask(task: Task)
@Delete
suspend fun deleteTask(task: Task)
@Query("DELETE FROM tasks WHERE isCompleted = 1")
suspend fun deleteCompletedTasks()
}
Return Flow<List<Task>> for reactive queries — Room automatically re-emits when the table data changes. Use suspend for one-shot writes.
Database Setup and Migrations
Building the Database
Room enforces that you use its builder to create the database instance. In production, ensure only one instance exists:
@Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
@Volatile
private var INSTANCE: TaskDatabase? = null
fun getInstance(context: Context): TaskDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TaskDatabase::class.java,
"task_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Migrations
When you change the schema (add column, create table), you must provide a migration. Without one, Room throws an IllegalStateException and your app crashes.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 0")
}
}
Register migrations when building the database:
Room.databaseBuilder(context, TaskDatabase::class.java, "task_database")
.addMigrations(MIGRATION_1_2)
.build()
Auto-Migrations (Room 2.4+)
For simple schema changes, Room can auto-generate migrations:
@Database(
entities = [Task::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
For complex migrations with data transformation, provide a spec:
@RenameColumn(tableName = "tasks", fromColumnName = "done", toColumnName = "isCompleted")
class Migration1To2 : AutoMigrationSpec
Testing Migrations
Room provides MigrationTestHelper to verify migrations in instrumented tests:
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
TaskDatabase::class.java
)
@Test
fun migrate1To2() {
helper.createDatabase("task_database", 1).apply {
execSQL("INSERT INTO tasks (title, description) VALUES ('Test', 'Desc')")
close()
}
helper.runMigrationsAndValidate("task_database", 2, MIGRATION_1_2).also {
// Verify schema and data
}
}
}
Always test migrations with real data. A broken migration means data loss.
Quiz
1. What does Room do when you change the database version without providing a migration?
2. Why should DAO query methods that return data use Flow instead of direct return values?
3. When should you use @TypeConverters in Room?
4. What is the purpose of the onConflict parameter in @Insert?
Flashcards
Question
What are the three main components of Room?
Click to reveal answer
Answer
Entity (maps to a table), DAO (defines database operations), and Database (holds the database instance and provides DAOs).
Question
What does @TypeConverters do in Room?
Click to reveal answer
Answer
Tells Room how to convert complex types (Date, custom objects) to and from database-storable primitives using @TypeConverter functions.
Question
When do you need a Room migration?
Click to reveal answer
Answer
Anytime you change the database schema after the initial creation — adding tables, columns, or modifying constraints. Room throws an error without one.
Question
What is the difference between @Insert, @Update, and @Delete in a DAO?
Click to reveal answer
Answer
@Insert adds a new row (with onConflict strategy), @Update modifies an existing row by primary key, @Delete removes a row by primary key. All can be suspend functions.
Revision Notes
Key Takeaways
- 1. Room enforces compile-time verification of SQL queries and database schema
- 2. Use Flow in DAOs for reactive queries that automatically update when data changes
- 3. Always provide a migration when changing the database version to avoid crashes
- 4. TypeConverters are required for any type Room does not natively support
- 5. Test migrations with real data to prevent data loss in production
Interview Tips
- • Explain Room's architecture: Entity, DAO, and Database components
- • Discuss how Room ensures compile-time query verification
- • Know how to write a manual migration and when to use AutoMigration
- • Explain why Flow is preferred over direct return values for queries
Cheat Sheet
Room Cheat Sheet
Entity:
@Entity(tableName = "table")— marks class as table@PrimaryKey(autoGenerate = true)— auto-incrementing ID@ColumnInfo(name = "col")— custom column name@Ignore— skip property from table
DAO:
@Query— custom SQL with:paramplaceholders@Insert(onConflict = REPLACE)— insert or replace@Update— update by primary key@Delete— delete by primary key- Return
Flow<T>for reactive queries - Use
suspendfor write operations
Database:
- Extend
RoomDatabase() - Mark with
@Database(entities, version) - Singleton pattern with
Room.databaseBuilder - Add migrations with
.addMigrations(...)
Migrations:
Migration(fromVersion, toVersion)— manual SQLAutoMigration(from, to)— Room-generated- Test with
MigrationTestHelper
Type Converters:
@TypeConverteron functions@TypeConverters(Converters::class)on database