Menyimpan data di database lokal menggunakan Room 2.x

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.

Gambar 1. Diagram arsitektur library 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 tableName di @Entity dan anotasi @ColumnInfo(name = "...").
  • Kunci utama: Untuk menentukan kunci utama, gunakan @PrimaryKey. Untuk kunci komposit, gunakan properti primaryKeys dari @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 Long yang merepresentasikan ID baris yang disisipkan, atau List<Long> yang berisi ID semua baris yang disisipkan.
  • Perbarui atau Hapus: Metode update atau hapus dapat menampilkan Int yang 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 @AutoMigration untuk otomatis memigrasikan perubahan skema dasar. Hal ini mengharuskan Anda menyetel exportSchema ke true dalam konfigurasi database: @Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]).
  • Penggantian destruktif: Jika hilangnya data dapat diterima saat jalur migrasi tidak ada, panggil .fallbackToDestructiveMigration saat 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:

  1. Perbarui dependensi untuk menyertakan Room.
  2. Anotasikan class model dengan @Entity, @PrimaryKey, dan @ColumnInfo.
  3. Buat DAO untuk menggantikan metode kueri helper Anda.
  4. Buat class RoomDatabase yang mereferensikan entity dan DAO Anda. Tingkatkan nomor versi.
  5. Tentukan jalur migrasi kosong karena skema tidak berubah, hanya framework: kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. Perbarui instansiasi untuk menggunakan Room.databaseBuilder dengan jalur migrasi.