Room Database di Android 2026: Migrasi, Relasi dan Coroutines

Kuasai Room database di Android dengan migrasi, relasi entitas, Kotlin coroutines dan Flow. Tutorial lengkap dengan pola production-ready untuk 2026.

Room Database Android Kotlin SQLite data persistence

Room database menyediakan lapisan abstraksi yang kuat di atas SQLite pada Android, menangani kode boilerplate yang membuat SQLite mentah menjadi membosankan untuk digunakan. Dengan Room 2.6 dan Kotlin coroutines, membangun lapisan database yang reaktif dan type-safe menjadi jauh lebih mudah.

Fitur Utama Room 2.6

Room 2.6 (stabil di 2026) memperkenalkan dukungan migrasi yang lebih baik, kecepatan pemrosesan KSP yang ditingkatkan, dan integrasi Flow yang lebih baik. Tutorial ini menggunakan API terbaru untuk kode production-ready.

Menyiapkan Room Database dengan Kotlin KSP

Room membutuhkan tiga komponen inti: entities (tabel), DAOs (data access objects), dan kelas database. Pemroses anotasi KSP menghasilkan kode implementasi pada waktu kompilasi.

Tambahkan dependencies ke file build.gradle.kts level modul. KSP menggantikan KAPT sebagai pemroses anotasi yang direkomendasikan sejak 2024, menawarkan waktu build yang lebih cepat.

build.gradle.kts (Module :app)kotlin
plugins {
    id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}

dependencies {
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion") // Coroutines support
    ksp("androidx.room:room-compiler:$roomVersion")
}

Artifak room-ktx menyediakan ekstensi coroutine, memungkinkan fungsi suspend dalam DAOs.

Mendefinisikan Entities dengan Primary Keys dan Indices

Entities dipetakan langsung ke tabel SQLite. Setiap kelas entity membutuhkan setidaknya satu primary key. Menambahkan indices pada kolom yang sering di-query meningkatkan performa baca dengan mengorbankan penulisan yang sedikit lebih lambat.

User.ktkotlin
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey

@Entity(
    tableName = "users",
    indices = [
        Index(value = ["email"], unique = true), // Unique constraint + index
        Index(value = ["created_at"]) // Index for sorting queries
    ]
)
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val email: String,
    val displayName: String,
    @ColumnInfo(name = "created_at")
    val createdAt: Long = System.currentTimeMillis(),
    val isActive: Boolean = true
)

Anotasi @ColumnInfo menyesuaikan nama kolom ketika nama properti Kotlin berbeda dari nama kolom database yang diinginkan.

Membangun DAOs dengan Coroutines dan Flow

DAOs mendefinisikan operasi database. Room 2.6 mendukung tiga pola async: fungsi suspend untuk operasi one-shot, Flow untuk stream reaktif, dan ListenableFuture untuk interop Java.

UserDao.ktkotlin
import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDao {
    // One-shot insert - returns the generated ID
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long

    // Batch insert - efficient for multiple records
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertUsers(users: List<User>)

    // Flow emits new data whenever the table changes
    @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
    fun observeActiveUsers(): Flow<List<User>>

    // Suspend function for one-shot read
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Long): User?

    // Update returns the number of affected rows
    @Update
    suspend fun updateUser(user: User): Int

    // Delete by primary key
    @Delete
    suspend fun deleteUser(user: User)

    // Custom delete query
    @Query("DELETE FROM users WHERE is_active = 0")
    suspend fun deleteInactiveUsers(): Int
}

Query yang mengembalikan Flow secara otomatis mengeluarkan hasil yang diperbarui ketika data yang mendasarinya berubah. Ini menghilangkan logika refresh manual di ViewModels.

Membuat Kelas Room Database

Kelas database mengikat entities dan DAOs bersama-sama. Pola singleton mencegah beberapa instance database, yang akan menyebabkan kebocoran resource.

AppDatabase.ktkotlin
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
    entities = [User::class, Post::class],
    version = 1,
    exportSchema = true // Generates schema JSON for migration validation
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun postDao(): PostDao

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

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .fallbackToDestructiveMigration() // Only for development
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Mengatur exportSchema = true menghasilkan file schema JSON di folder schemas/, berguna untuk memvalidasi migrasi dalam pengujian.

Strategi Migrasi Produksi

Jangan pernah menggunakan fallbackToDestructiveMigration() di produksi. Ini menghapus semua data pengguna ketika skema berubah. Bagian selanjutnya membahas penanganan migrasi yang tepat.

Migrasi mempertahankan data pengguna ketika skema berubah antar versi aplikasi. Room memvalidasi migrasi saat runtime dengan membandingkan skema yang diharapkan dengan database yang telah dimigrasi.

Migrasi mendefinisikan pernyataan SQL yang diperlukan untuk mentransformasi database dari versi N ke versi N+1.

Migrations.ktkotlin
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

// Migration from version 1 to 2: add phone_number column
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT")
    }
}

// Migration from version 2 to 3: add posts table with foreign key
val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("""
            CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                user_id INTEGER NOT NULL,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at INTEGER NOT NULL,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
            )
        """.trimIndent())
        db.execSQL("CREATE INDEX index_posts_user_id ON posts(user_id)")
    }
}

// Register migrations in database builder
fun getInstance(context: Context): AppDatabase {
    return INSTANCE ?: synchronized(this) {
        Room.databaseBuilder(
            context.applicationContext,
            AppDatabase::class.java,
            "app_database"
        )
            .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
            .build()
            .also { INSTANCE = it }
    }
}

Room mendukung chaining migrasi. Jika pengguna upgrade dari versi 1 ke versi 3, Room mengeksekusi MIGRATION_1_2 kemudian MIGRATION_2_3 secara berurutan.

Auto-Migrations di Room 2.6

Untuk perubahan skema sederhana (menambahkan kolom, tabel, atau indices), Room dapat menghasilkan migrasi secara otomatis. Ini mengurangi boilerplate untuk update yang straightforward.

AppDatabase.ktkotlin
@Database(
    entities = [User::class, Post::class],
    version = 3,
    autoMigrations = [
        AutoMigration(from = 1, to = 2), // Adding nullable column
        AutoMigration(
            from = 2,
            to = 3,
            spec = AutoMigration2To3::class // Complex changes need spec
        )
    ],
    exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
    // DAOs...
}

// Migration spec for changes that need hints
@RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "display_name")
class AutoMigration2To3 : AutoMigrationSpec

Auto-migrations memerlukan exportSchema = true untuk membandingkan versi. Untuk rename kolom, rename tabel, atau penghapusan, berikan AutoMigrationSpec dengan anotasi yang sesuai.

Siap menguasai wawancara Android Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Memodelkan Relasi Entity dengan Embedded Objects

Room mendukung tiga pola relasi: embedded objects, one-to-many, dan many-to-many. Tidak seperti ORMs, Room mengembalikan data flat—relasi memerlukan query eksplisit.

One-to-Many: User dengan Posts

Seorang user dapat memiliki beberapa posts. Definisikan relasi menggunakan anotasi @Relation dalam data class yang menggabungkan kedua entities.

Post.ktkotlin
@Entity(
    tableName = "posts",
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["user_id"],
            onDelete = ForeignKey.CASCADE // Delete posts when user deleted
        )
    ],
    indices = [Index("user_id")] // Required for foreign key performance
)
data class Post(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    @ColumnInfo(name = "user_id") val userId: Long,
    val title: String,
    val content: String,
    @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

// UserWithPosts.kt - Relation container
data class UserWithPosts(
    @Embedded val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "user_id"
    )
    val posts: List<Post>
)

// In UserDao.kt
@Transaction // Ensures atomic read of user + posts
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserWithPosts(userId: Long): UserWithPosts?

@Transaction
@Query("SELECT * FROM users WHERE is_active = 1")
fun observeActiveUsersWithPosts(): Flow<List<UserWithPosts>>

Anotasi @Transaction mencegah pembacaan yang tidak konsisten ketika relasi mencakup beberapa query secara internal.

Many-to-Many: Users dan Tags

Relasi many-to-many memerlukan tabel junction (cross-reference) yang menyimpan pasangan foreign keys.

Tag.ktkotlin
@Entity(tableName = "tags")
data class Tag(
    @PrimaryKey(autoGenerate = true) val tagId: Long = 0,
    val name: String
)

// UserTagCrossRef.kt - Junction table
@Entity(
    tableName = "user_tag_cross_ref",
    primaryKeys = ["userId", "tagId"],
    foreignKeys = [
        ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE),
        ForeignKey(entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE)
    ]
)
data class UserTagCrossRef(
    val userId: Long,
    val tagId: Long
)

// UserWithTags.kt
data class UserWithTags(
    @Embedded val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "tagId",
        associateBy = Junction(UserTagCrossRef::class)
    )
    val tags: List<Tag>
)

Parameter associateBy menentukan tabel junction yang menghubungkan users ke tags.

Mengintegrasikan Room dengan ViewModel dan Repository

Arsitektur bersih memisahkan lapisan database dari UI. Pola repository mengabstraksi sumber data, sementara ViewModel mengekspos state ke Compose UI.

UserRepository.ktkotlin
class UserRepository(private val userDao: UserDao) {

    val activeUsers: Flow<List<User>> = userDao.observeActiveUsers()

    suspend fun createUser(email: String, displayName: String): Long {
        val user = User(email = email, displayName = displayName)
        return userDao.insertUser(user)
    }

    suspend fun deactivateUser(userId: Long) {
        userDao.getUserById(userId)?.let { user ->
            userDao.updateUser(user.copy(isActive = false))
        }
    }
}

// UserViewModel.kt
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    val users: StateFlow<List<User>> = repository.activeUsers
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun createUser(email: String, name: String) {
        viewModelScope.launch {
            repository.createUser(email, name)
            // No manual refresh needed - Flow emits automatically
        }
    }
}

Operator stateIn mengkonversi cold Flow dari Room menjadi hot StateFlow yang cocok untuk pengumpulan Compose. Untuk detail lebih lanjut tentang pola coroutines, lihat panduan tentang Kotlin Coroutines.

Menguji Room Databases

Room menyediakan builder database in-memory untuk pengujian: Room.inMemoryDatabaseBuilder(). Pengujian berjalan lebih cepat dan tidak mempertahankan data antar run. Selalu uji migrasi dengan MigrationTestHelper dari artifak room-testing.

Type Converters untuk Tipe Kompleks

Room hanya menyimpan tipe primitif secara native. Type converters mentransformasi tipe kompleks (tanggal, enums, lists) ke format yang dapat disimpan.

Converters.ktkotlin
import androidx.room.TypeConverter
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset

class Converters {
    // Instant <-> Long (milliseconds)
    @TypeConverter
    fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli()

    @TypeConverter
    fun toInstant(millis: Long?): Instant? = millis?.let { Instant.ofEpochMilli(it) }

    // LocalDate <-> String (ISO format)
    @TypeConverter
    fun fromLocalDate(date: LocalDate?): String? = date?.toString()

    @TypeConverter
    fun toLocalDate(dateStr: String?): LocalDate? = dateStr?.let { LocalDate.parse(it) }

    // List<String> <-> JSON string
    @TypeConverter
    fun fromStringList(list: List<String>?): String? =
        list?.joinToString(separator = ",")

    @TypeConverter
    fun toStringList(data: String?): List<String>? =
        data?.split(",")?.filter { it.isNotBlank() }
}

// Register in database class
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

Untuk serialisasi JSON dari objek kompleks, pertimbangkan menggunakan kotlinx.serialization daripada manipulasi string manual.

Menangani Operasi Database dalam Coroutines Scope

Operasi DAO Room harus berjalan di IO dispatcher. Meskipun Room menangani threading secara internal untuk fungsi suspend, penanganan error di tingkat repository menjaga kode tetap robust.

UserRepository.kt with error handlingkotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class UserRepository(private val userDao: UserDao) {

    suspend fun createUser(email: String, displayName: String): Result<Long> {
        return withContext(Dispatchers.IO) {
            try {
                val id = userDao.insertUser(
                    User(email = email, displayName = displayName)
                )
                Result.success(id)
            } catch (e: SQLiteConstraintException) {
                // Unique constraint violation (duplicate email)
                Result.failure(DuplicateEmailException(email))
            }
        }
    }

    suspend fun deleteAllInactive(): Int = withContext(Dispatchers.IO) {
        userDao.deleteInactiveUsers()
    }
}

class DuplicateEmailException(email: String) : Exception("Email already exists: $email")

Membungkus hasil dalam Result<T> memungkinkan ViewModels menangani error dengan elegan tanpa boilerplate try-catch.

Kesimpulan

Membangun lapisan Room database yang production-ready melibatkan beberapa praktik kunci:

  • Gunakan KSP daripada KAPT untuk kompilasi yang lebih cepat dengan Room 2.6
  • Tambahkan indices pada kolom yang digunakan dalam klausa WHERE dan kondisi JOIN
  • Kembalikan Flow dari DAOs untuk pembaruan UI otomatis ketika data berubah
  • Tulis migrasi eksplisit untuk perubahan skema—jangan pernah gunakan migrasi destruktif di produksi
  • Gunakan @Transaction untuk query relasi untuk memastikan pembacaan yang konsisten
  • Bungkus operasi database dalam Result untuk penanganan error yang bersih di ViewModels
  • Uji migrasi dengan MigrationTestHelper sebelum merilis pembaruan

Latih pola-pola ini dengan pertanyaan interview Room database untuk memperkuat konsep-konsep tersebut.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#android
#room
#kotlin
#sqlite
#coroutines
#database

Bagikan

Artikel terkait