Jika aplikasi Anda menangani data terstruktur dalam jumlah sangat banyak, Anda akan sangat terbantu jika data tersebut disimpan secara lokal. Kasus penggunaan yang paling umum adalah menyimpan bagian data yang relevan ke dalam cache sehingga jika perangkat tidak dapat mengakses jaringan, Anda masih dapat menjelajahi konten tersebut meskipun offline.
Library persistensi Room menyediakan lapisan abstraksi pada SQLite untuk memungkinkan Anda mengakses database dengan lancar sambil memanfaatkan kemampuan penuh SQLite.
Menyiapkan Room 2.x
Untuk menggunakan Room 2.x di aplikasi Anda, tambahkan dependensi berikut ke file
build.gradle aplikasi Anda:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
Komponen utama
Room memiliki tiga komponen utama:
- Class database yang menyimpan database dan berfungsi sebagai titik akses utama bagi koneksi saat ini ke data persisten aplikasi Anda.
- Entity data yang menampilkan tabel di database aplikasi Anda.
- Objek akses data (DAO) yang menyediakan metode yang dapat digunakan aplikasi Anda untuk membuat kueri, mengupdate, menyisipkan, dan menghapus data dalam database.
Gambar 1 mengilustrasikan hubungan antara berbagai komponen Room.
Contoh penerapan
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Menentukan data menggunakan entity
Setiap entity Room mewakili tabel dalam database. Anda menentukan setiap entity sebagai
class yang dianotasikan dengan @Entity.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- Nama tabel dan kolom kustom: Secara default, Room menggunakan nama class sebagai nama tabel dan nama properti sebagai nama kolom. Untuk menyesuaikannya, gunakan
properti
tableNamedi@Entitydan anotasi@ColumnInfo(name = "..."). - Kunci utama: Untuk menentukan kunci utama, gunakan
@PrimaryKey. Untuk kunci komposit, gunakan propertiprimaryKeysdari@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Mengabaikan kolom: Untuk mencegah kolom dipertahankan, gunakan
@Ignore.
Konverter jenis
Terkadang, Anda perlu menyimpan jenis kustom, seperti Date, dalam satu kolom.
Menyediakan metode @TypeConverter untuk mengonversi jenis kustom ke dan dari jenis yang dapat dipertahankan oleh Room.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
Mengakses data menggunakan DAO
DAO menentukan metode untuk interaksi database. Anotasikan antarmuka atau class abstrak dengan @Dao.
Metode praktis
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Sisipkan: Metode penyisipan dapat menampilkan
Longyang merepresentasikan ID baris yang disisipkan, atauList<Long>yang berisi ID semua baris yang disisipkan. - Perbarui atau Hapus: Metode update atau hapus dapat menampilkan
Intyang merepresentasikan jumlah baris yang terpengaruh.
Metode kueri
Anotasi metode dengan @Query untuk menulis pernyataan SQL. Room memvalidasi
kueri pada waktu kompilasi.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
Jenis nilai yang ditampilkan multimap
Di Room 2.4 dan yang lebih tinggi, metode kueri dapat menampilkan multimap secara langsung menggunakan
jenis Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Kueri DAO asinkron
Untuk menghindari pembekuan UI, kueri database tidak dapat berjalan di thread utama. Buat kueri asinkron menggunakan salah satu integrasi berikut:
Coroutine dan Flow Kotlin
Memerlukan dependensi room-ktx.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java dengan RxJava
Memerlukan room-rxjava2 atau room-rxjava3.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java dengan LiveData dan Guava
Memerlukan room-guava untuk ListenableFuture.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Menentukan hubungan di Room 2.x
Untuk mencegah pemuatan lambat di UI thread, Anda tidak dapat menggunakan referensi objek langsung antar-entitas. Sebagai gantinya, tetapkan hubungan menggunakan class data perantara dengan @Relation.
One-to-one
Setiap pengguna hanya memiliki satu library.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
One-to-many
Setiap pengguna dapat memiliki banyak playlist.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
Many-to-many
Playlist dapat berisi banyak lagu, dan lagu dapat berada di banyak playlist. Memerlukan tabel persimpangan.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
Hubungan bertingkat
Kueri pengguna, playlist mereka, dan semua lagu dalam playlist tersebut.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Pengelolaan database
Bagian ini mencakup berbagai aspek pengelolaan database Room Anda, termasuk tampilan database, pengisian otomatis data, dan migrasi database.
Tampilan database
Enkapsulasi kueri kompleks ke dalam class yang dianotasi dengan @DatabaseView.
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
Mengisi otomatis database
Isi database saat inisialisasi dari file aset atau sistem file.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migrasi
Saat mengubah skema, naikkan versi database dan tentukan objek
Migration.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- Migrasi otomatis: Jika menggunakan Room 2.4.0 atau yang lebih tinggi, Anda dapat menggunakan
@AutoMigrationuntuk otomatis memigrasikan perubahan skema dasar. Hal ini mengharuskan Anda menyetelexportSchemaketruedalam konfigurasi database:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Penggantian destruktif: Jika hilangnya data dapat diterima saat jalur migrasi tidak ada, panggil
.fallbackToDestructiveMigrationsaat membangun database.
Menguji migrasi
Untuk memverifikasi migrasi, gunakan MigrationTestHelper dari artefak room-testing. Untuk mendukung hal ini, pastikan Anda mengekspor skema dalam konfigurasi
build.gradle.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
Bermigrasi dari SQLite ke Room
Untuk memigrasikan aplikasi Anda dari SQLite ke Room, selesaikan langkah-langkah berikut:
- Perbarui dependensi untuk menyertakan Room.
- Anotasikan class model dengan
@Entity,@PrimaryKey, dan@ColumnInfo. - Buat DAO untuk menggantikan metode kueri helper Anda.
- Buat class RoomDatabase yang mereferensikan entity dan DAO Anda. Tingkatkan nomor versi.
- Tentukan jalur migrasi kosong karena skema tidak berubah, hanya
framework:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Perbarui instansiasi untuk menggunakan
Room.databaseBuilderdengan jalur migrasi.