androidengineers.Book a session

Database Design

Room: Entities, DAOs, Converters

article25 minMedium

Room is Android's official SQLite abstraction library. It provides compile-time SQL verification, reactive queries via Flow, and a clean annotation-driven API. Understanding entities, DAOs, and type converters is the foundation of any Room-based data layer.

Entities: Mapping Tables

@Entity(
    tableName = "articles",
    indices = [
        Index(value = ["author_id"]),           // for fast author queries
        Index(value = ["title"], unique = true)  // no duplicate titles
    ],
    foreignKeys = [
        ForeignKey(
            entity = AuthorEntity::class,
            parentColumns = ["id"],
            childColumns = ["author_id"],
            onDelete = ForeignKey.CASCADE  // delete articles when author deleted
        )
    ]
)
data class ArticleEntity(
    @PrimaryKey val id: String,
    val title: String,
    val body: String,
    @ColumnInfo(name = "author_id") val authorId: String,
    @ColumnInfo(name = "published_at") val publishedAt: Long,
    @ColumnInfo(name = "cached_at") val cachedAt: Long = System.currentTimeMillis()
)

DAOs: Typed Query Methods

@Dao
interface ArticleDao {
    // Suspend functions for one-shot operations
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(article: ArticleEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(articles: List<ArticleEntity>)

    @Update
    suspend fun update(article: ArticleEntity)

    @Delete
    suspend fun delete(article: ArticleEntity)

    @Query("DELETE FROM articles WHERE id = :id")
    suspend fun deleteById(id: String)

    @Query("DELETE FROM articles")
    suspend fun deleteAll()

    // Flow for reactive queries — emits on every table change
    @Query("SELECT * FROM articles ORDER BY published_at DESC")
    fun getAllAsFlow(): Flow<List<ArticleEntity>>

    @Query("SELECT * FROM articles WHERE id = :id")
    fun getByIdAsFlow(id: String): Flow<ArticleEntity?>

    // Suspend for one-shot reads
    @Query("SELECT * FROM articles WHERE id = :id")
    suspend fun getById(id: String): ArticleEntity?

    // JOIN query — Room maps result to a POJO
    @Query("""
        SELECT a.*, u.name as author_name, u.avatar_url as author_avatar
        FROM articles a
        INNER JOIN authors u ON a.author_id = u.id
        WHERE a.id = :id
    """)
    suspend fun getArticleWithAuthor(id: String): ArticleWithAuthor?

    // Pagination with Paging 3
    @Query("SELECT * FROM articles ORDER BY published_at DESC")
    fun pagingSource(): PagingSource<Int, ArticleEntity>
}

data class ArticleWithAuthor(
    @Embedded val article: ArticleEntity,
    @ColumnInfo(name = "author_name") val authorName: String,
    @ColumnInfo(name = "author_avatar") val authorAvatar: String
)

Type Converters

Room stores only primitives and Strings natively. For custom types, provide converters:

class Converters {
    // List<String> ↔ JSON String
    @TypeConverter
    fun fromStringList(value: List<String>): String = Json.encodeToString(value)

    @TypeConverter
    fun toStringList(value: String): List<String> = Json.decodeFromString(value)

    // Enum ↔ String
    @TypeConverter
    fun fromStatus(status: ArticleStatus): String = status.name

    @TypeConverter
    fun toStatus(value: String): ArticleStatus = ArticleStatus.valueOf(value)

    // Custom data class ↔ JSON
    @TypeConverter
    fun fromMetadata(meta: ArticleMetadata?): String? =
        meta?.let { Json.encodeToString(it) }

    @TypeConverter
    fun toMetadata(value: String?): ArticleMetadata? =
        value?.let { Json.decodeFromString(it) }
}

enum class ArticleStatus { DRAFT, PUBLISHED, ARCHIVED }

@Serializable
data class ArticleMetadata(val wordCount: Int, val readTimeMinutes: Int)

Register converters on the database:

@Database(entities = [ArticleEntity::class, AuthorEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun articleDao(): ArticleDao
    abstract fun authorDao(): AuthorDao

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

        fun getInstance(context: Context): AppDatabase = INSTANCE ?: synchronized(this) {
            INSTANCE ?: Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app.db"
            ).build().also { INSTANCE = it }
        }
    }
}

Relationships: @Relation

data class AuthorWithArticles(
    @Embedded val author: AuthorEntity,
    @Relation(
        parentColumn = "id",
        entityColumn = "author_id"
    )
    val articles: List<ArticleEntity>
)

// In AuthorDao:
@Transaction
@Query("SELECT * FROM authors WHERE id = :authorId")
suspend fun getAuthorWithArticles(authorId: String): AuthorWithArticles?

Always use @Transaction with @Relation — Room makes two separate queries and needs a transaction to ensure consistency.

Key Takeaways

ConceptRule
@Insert(onConflict = REPLACE)Use for upserts in cache scenarios
Flow returnsUse for reactive UI; Room emits on any change
Suspend functionsUse for one-shot operations (insert, delete)
@TypeConverterRequired for enums, lists, custom objects
@Relation + @TransactionAlways pair these — @Transaction is mandatory
@ColumnInfo(name = ...)Keep DB column names stable even when you rename Kotlin properties

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Room: Entities, DAOs, Converters | Android System Design | Android Engineers