Aplikasi Android yang menyimpan data persisten biasanya memakai SQLite. Menulis SQL mentah melalui SQLiteOpenHelper berisiko: typo pada nama kolom tidak terdeteksi sampai runtime, dan mapping Cursor ke objek Kotlin sangat membosankan. Room, library Jetpack, menambahkan lapisan abstraksi di atas SQLite dengan anotasi @Database, @Entity, dan @Dao. Kompilator memverifikasi SQL saat build, sehingga kesalahan ketik terdeteksi sebelum APK dipublikasikan. Bab ini menjelaskan ketiga anotasi inti Room, pola migrasi skema, dan integrasi dengan coroutines.
Tiga Anotasi Inti Room #
Room bekerja dengan tiga anotasi: @Database menandai kelas abstract yang menjadi titik masuk, @Entity menandai class data yang dipetakan ke tabel, dan @Dao menandai interface yang berisi query SQL.
@Entity(tableName = "tugas")
data class TugasEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "judul") val judul: String,
@ColumnInfo(name = "selesai") val selesai: Boolean = false
)
@Dao
interface TugasDao {
@Query("SELECT * FROM tugas ORDER BY id")
fun getAll(): Flow<List<TugasEntity>>
@Query("SELECT * FROM tugas WHERE id = :id")
suspend fun findById(id: Int): TugasEntity?
@Insert
suspend fun insert(tugas: TugasEntity)
@Update
suspend fun update(tugas: TugasEntity)
@Delete
suspend fun delete(tugas: TugasEntity)
}
@Database(entities = [TugasEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun tugasDao(): TugasDao
}Saat kompilator memproses kode di atas, ia memeriksa bahwa setiap @Query valid: nama tabel, kolom, dan parameter sesuai. Bila ada typo (mis. judu alih-alih judul), build gagal dengan pesan yang jelas.
Verifikasi SQL Saat Kompilasi #
Kompilator Room memeriksa SQL pada anotasi @Query, @Insert, @Update, dan @Delete. Setiap parameter :id harus cocok dengan parameter method. Setiap nama kolom harus ada di entitas. Verifikasi ini mencegah bug runtime yang sulit dilacak.
| Kondisi | Hasil build |
|---|---|
@Query("SELECT * FROM tugas WHERE id = :id") dengan fun findById(id: Int) | OK |
@Query("SELECT * FROM tugas WHERE id = :id") tanpa parameter method | Fail |
@Query("SELECT judu FROM tugas") (kolom typo) | Fail |
@Insert pada entitas yang tidak terdaftar di @Database | Fail |
Migrasi Skema #
Saat kita mengubah entitas (tambah kolom, ubah tipe, hapus tabel), versi database harus dinaikkan dan Migration harus disediakan. Tanpa migration, Room akan IllegalStateException karena data pengguna akan hilang.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE tugas ADD COLUMN prioritas INTEGER NOT NULL DEFAULT 0")
}
}
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build()Untuk skenario dev, kita dapat memakai fallbackToDestructiveMigration() yang menghapus seluruh data dan membuat ulang skema. Opsi ini tidak boleh dipakai di production karena menghapus data pengguna.
Simulasi Room sebagai Struktur Data #
Karena Room memerlukan Android SDK, cuplikan berikut memodelkan @Entity, @Dao, @Database, dan Migration sebagai struktur data ringkas. Operasi DAO (getAll, insert, update, delete) ditampilkan dengan komentar SQL yang setara:
// room_database.kt
// Mensimulasikan konsep Room: @Database, @Entity, @Dao, dan migration
// sebagai struktur data ringkas yang dapat dijalankan tanpa Android SDK.
// Pada aplikasi nyata, anotasi diproses oleh KSP kompiler plugin Room.
data class TugasEntity(
val id: Int,
val judul: String,
val selesai: Boolean = false,
val versiSkema: Int = 1
)
// Mensimulasikan @Dao — data access object yang berisi query SQL
class TugasDao(private val table: MutableList<TugasEntity>) {
// @Query("SELECT * FROM tugas ORDER BY id")
fun getAll(): List<TugasEntity> = table.sortedBy { it.id }
// @Query("SELECT * FROM tugas WHERE id = :id")
fun findById(id: Int): TugasEntity? = table.firstOrNull { it.id == id }
// @Insert
fun insert(tugas: TugasEntity) { table.add(tugas) }
// @Update
fun update(tugas: TugasEntity) {
val idx = table.indexOfFirst { it.id == tugas.id }
if (idx >= 0) table[idx] = tugas
}
// @Delete
fun delete(tugas: TugasEntity) { table.remove(tugas) }
}
// Mensimulasikan RoomDatabase dengan migrasi
class RoomDatabase private constructor(
private val dao: TugasDao,
var versi: Int
) {
companion object {
fun build(initial: List<TugasEntity> = emptyList(), versi: Int = 1): RoomDatabase {
return RoomDatabase(TugasDao(initial.toMutableList()), versi)
}
}
fun tugasDao(): TugasDao = dao
}
// Migrasi dari versi 1 -> 2: tambah kolom "versiSkema"
object Migration1To2 {
fun migrate(db: RoomDatabase) {
val dao = db.tugasDao()
dao.getAll().forEach { tugas ->
if (tugas.versiSkema < 2) {
dao.update(tugas.copy(versiSkema = 2))
}
}
db.versi = 2
}
}
fun main() {
val db = RoomDatabase.build(
initial = listOf(
TugasEntity(1, "Belanja", false, versiSkema = 1),
TugasEntity(2, "Olahraga", true, versiSkema = 1)
),
versi = 1
)
val dao = db.tugasDao()
println("=== Versi 1 (sebelum migrasi) ===")
dao.getAll().forEach { println(" $it") }
println("\n=== Insert tugas baru ===")
dao.insert(TugasEntity(3, "Belajar Kotlin", false, versiSkema = 1))
dao.getAll().forEach { println(" $it") }
println("\n=== Update status tugas id=1 ===")
dao.update(dao.findById(1)!!.copy(selesai = true))
dao.getAll().forEach { println(" $it") }
println("\n=== Migrasi ke versi 2 ===")
Migration1To2.migrate(db)
println("Versi DB: ${db.versi}")
dao.getAll().forEach { println(" $it") }
println("\n=== Delete tugas id=2 ===")
dao.delete(dao.findById(2)!!)
dao.getAll().forEach { println(" $it") }
}Output yang Dihasilkan:
=== Versi 1 (sebelum migrasi) ===
TugasEntity(id=1, judul=Belanja, selesai=false, versiSkema=1)
TugasEntity(id=2, judul=Olahraga, selesai=true, versiSkema=1)
=== Insert tugas baru ===
TugasEntity(id=1, judul=Belanja, selesai=false, versiSkema=1)
TugasEntity(id=2, judul=Olahraga, selesai=true, versiSkema=1)
TugasEntity(id=3, judul=Belajar Kotlin, selesai=false, versiSkema=1)
=== Update status tugas id=1 ===
TugasEntity(id=1, judul=Belanja, selesai=true, versiSkema=1)
TugasEntity(id=2, judul=Olahraga, selesai=true, versiSkema=1)
TugasEntity(id=3, judul=Belajar Kotlin, selesai=false, versiSkema=1)
=== Migrasi ke versi 2 ===
Versi DB: 2
TugasEntity(id=1, judul=Belanja, selesai=true, versiSkema=2)
TugasEntity(id=2, judul=Olahraga, selesai=true, versiSkema=2)
TugasEntity(id=3, judul=Belajar Kotlin, selesai=false, versiSkema=2)
=== Delete tugas id=2 ===
TugasEntity(id=1, judul=Belanja, selesai=true, versiSkema=2)
TugasEntity(id=3, judul=Belajar Kotlin, selesai=false, versiSkema=2)Perhatikan bahwa setelah migrasi, semua entitas memiliki versiSkema = 2. Pola ini mencerminkan bagaimana Room memigrasi skema: operasi dijalankan satu per satu pada data yang ada, lalu versi database dinaikkan.
Referensi Lanjutan #
- Save data in a local database using Room — dokumentasi resmi pustaka Room Jetpack untuk penyimpanan data lokal berbasis SQLite
Selanjutnya #
Room cocok untuk data terstruktur, namun untuk key-value sederhana (preference, flag), DataStore lebih ringan. Bab DataStore akan membahas pengganti SharedPreferences dengan API async.