Mengakses data menggunakan DAO Room

Selama menggunakan library persistensi Room untuk menyimpan data aplikasi, Anda berinteraksi dengan data yang disimpan dengan menentukan objek akses data, atau DAO. Setiap DAO menyertakan fungsi yang menawarkan akses abstrak ke database aplikasi Anda. Pada waktu kompilasi, Room otomatis akan membuat implementasi DAO yang Anda tentukan.

Dengan menggunakan DAO untuk mengakses database aplikasi, bukan builder kueri atau kueri langsung, Anda dapat mempertahankan pemisahan fokus yang merupakan prinsip penting dalam arsitektur. DAO juga memungkinkan Anda meniru akses database saat menguji aplikasi .

Anatomi DAO

Anda dapat menentukan setiap DAO sebagai antarmuka atau class abstrak. Untuk kasus penggunaan dasar, biasanya Anda menggunakan antarmuka. Dalam kedua kasus tersebut, Anda harus selalu menganotasi DAO dengan @Dao. DAO tidak memiliki properti, tetapi menentukan satu atau beberapa fungsi untuk berinteraksi dengan data di database aplikasi Anda.

Kode berikut adalah contoh DAO yang menentukan fungsi untuk menyisipkan, menghapus, dan memilih objek User dalam database Room:

@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

Ada dua jenis fungsi DAO yang menentukan interaksi database:

  • Fungsi praktis yang memungkinkan Anda menyisipkan, memperbarui, dan menghapus baris di database tanpa harus menulis kode SQL.
  • Fungsi kueri yang memungkinkan untuk menulis kueri SQL Anda sendiri untuk berinteraksi dengan database.

Bagian berikut menunjukkan cara menggunakan kedua jenis fungsi DAO untuk menentukan interaksi database yang diperlukan aplikasi.

Fungsi praktis

Room menyediakan anotasi praktis untuk menentukan fungsi yang menjalankan penyisipan, pembaruan, dan penghapusan tanpa mengharuskan Anda menulis pernyataan SQL.

Jika Anda harus menentukan penyisipan, pembaruan, atau penghapusan yang lebih kompleks, atau jika Anda perlu membuat kueri data dalam database, gunakan fungsi kueri sebagai gantinya.

Sisipkan

Anotasi @Insert memungkinkan Anda menentukan fungsi yang menyisipkan parameternya ke dalam tabel yang sesuai di database. Kode berikut menunjukkan contoh fungsi @Insert yang valid yang menyisipkan satu atau beberapa objek User ke dalam database:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Insert
    suspend fun insertBothUsers(user1: User, user2: User)

    @Insert
    suspend fun insertUsersAndFriends(user: User, friends: List<User>)
}

Setiap parameter untuk fungsi @Insert harus berupa instance class entity data Room yang dianotasikan dengan @Entity atau kumpulan instance class entity data . Saat fungsi @Insert dipanggil, Room akan menyisipkan setiap instance entity yang diteruskan ke tabel database yang bersangkutan.

Jika fungsi @Insert menerima satu parameter, fungsi ini dapat menampilkan nilai Long, yang merupakan rowId baru untuk item yang disisipkan. Jika parameter adalah array atau kumpulan, parameter harus menampilkan array atau kumpulan nilai Long sebagai gantinya, dengan setiap nilai sebagai rowId untuk salah satu item yang disisipkan. Untuk mempelajari lebih lanjut cara menampilkan nilai rowId, lihat dokumentasi referensi untuk anotasi @Insert dan Dokumentasi SQLite untuk tabel rowid.

Perbarui

Anotasi @Update memungkinkan Anda menentukan fungsi yang memperbarui baris tertentu dalam tabel database. Seperti fungsi @Insert, fungsi @Update menerima instance entity data sebagai parameter. Kode berikut menunjukkan contoh fungsi @Update yang mencoba memperbarui satu atau beberapa objek User di database:

@Dao
interface UserDao {
    @Update
    suspend fun updateUsers(vararg users: User)
}

Room menggunakan kunci utama untuk mencocokkan instance entity dalam argumen ke baris dalam database. Jika tidak ada baris dengan kunci utama yang sama, Room tidak akan membuat perubahan.

Fungsi @Update secara opsional dapat menampilkan nilai Int yang menunjukkan jumlah baris yang berhasil diperbarui.

Hapus

Anotasi @Delete memungkinkan Anda menentukan fungsi yang menghapus baris tertentu dari tabel database. Seperti fungsi @Insert, fungsi @Delete menerima instance entity data sebagai parameter. Kode berikut menunjukkan contoh fungsi @Delete yang mencoba menghapus satu atau beberapa objek User dari database:

@Dao
interface UserDao {
    @Delete
    suspend fun deleteUsers(vararg users: User)
}

Room menggunakan kunci utama untuk mencocokkan instance entity dalam argumen ke baris dalam database. Jika tidak ada baris dengan kunci utama yang sama, Room tidak akan membuat perubahan.

Fungsi @Delete secara opsional dapat menampilkan nilai Int yang menunjukkan jumlah baris yang berhasil dihapus.

Upsert

Anotasi @Upsert memungkinkan Anda menentukan fungsi yang menyisipkan instance entity jika tidak ada baris yang cocok, atau memperbaruinya jika baris sudah ada dengan kunci utama yang sama.

Seperti fungsi @Insert dan @Update, fungsi @Upsert menerima instance entity data sebagai parameter. Kode berikut menunjukkan contoh fungsi @Upsert yang mencoba melakukan upsert satu atau beberapa objek User di database:

@Dao
interface UserDao {
    @Upsert
    suspend fun upsertUsers(vararg users: User)
}

Jika fungsi @Upsert menerima satu parameter, fungsi ini dapat menampilkan nilai Long. Jika hasilnya adalah penyisipan baris baru, fungsi ini akan menampilkan rowId dari baris yang baru disisipkan. Jika hasilnya adalah pembaruan baris yang ada, fungsi ini akan menampilkan -1. Jika parameter adalah array atau kumpulan, parameter harus menampilkan array atau kumpulan nilai Long sebagai gantinya.

Fungsi kueri

Anotasi @Query memungkinkan Anda menulis pernyataan SQL dan menampilkannya sebagai fungsi DAO. Gunakan fungsi kueri ini untuk mengueri data dari database aplikasi, atau ketika perlu melakukan penyisipan, pembaruan, dan penghapusan yang lebih kompleks.

Room memvalidasi kueri SQL pada waktu kompilasi. Ini artinya bahwa akan terjadi error kompilasi, bukan kegagalan runtime, jika ada masalah dengan kueri Anda.

Kueri sederhana

Kode berikut menentukan fungsi yang menggunakan kueri SELECT untuk menampilkan semua objek User dalam database:

@Query("SELECT * FROM user")
suspend fun loadAllUsers(): List<User>

Bagian berikut menunjukkan cara memodifikasi contoh ini untuk kasus penggunaan umum.

Menampilkan subset kolom tabel

Sering kali, Anda hanya perlu menampilkan subset kolom dari tabel yang Anda kueri. Misalnya, UI Anda mungkin hanya menampilkan nama depan dan nama belakang pengguna, bukan setiap detail pengguna tersebut. Untuk menghemat resource dan menyederhanakan eksekusi kueri, cukup buat kueri properti yang Anda butuhkan.

Dengan Room, Anda dapat menampilkan objek data dari kueri mana pun selama Anda dapat memetakan kumpulan kolom hasil ke objek yang ditampilkan. Misalnya, Anda dapat menentukan objek berikut untuk menyimpan nama depan dan nama belakang pengguna:

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)

Kemudian, Anda dapat menampilkan objek data tersebut dari fungsi kueri:

@Query("SELECT first_name, last_name FROM user")
suspend fun loadFullName(): List<NameTuple>

Karena kueri menampilkan nilai untuk kolom first_name dan last_name, Room memetakan nilai ini ke properti di class NameTuple. Jika kueri menampilkan kolom yang tidak dipetakan ke properti dalam objek yang ditampilkan, Room akan menampilkan peringatan.

Meskipun contoh sebelumnya menggunakan class data kustom untuk mengambil subset kolom, Room juga mendukung menampilkan kotlin.Pair dan kotlin.Triple untuk memudahkan saat kueri menampilkan tepat dua atau tiga kolom. Saat menggunakan jenis ini, kolom dipetakan berdasarkan urutan kolom yang ditentukan dalam pernyataan kueri, sehingga urutan kolom dalam pernyataan SELECT harus cocok dengan urutan jenis dalam Pair atau Triple.

Meneruskan parameter sederhana ke kueri

Sering kali, fungsi DAO harus menerima parameter agar dapat menjalankan operasi pemfilteran. Room mendukung penggunaan parameter fungsi sebagai parameter binding di kueri Anda.

Misalnya, kode berikut menentukan fungsi yang menampilkan semua pengguna di atas usia tertentu:

@Query("SELECT * FROM user WHERE age > :minAge")
suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>

Anda juga dapat meneruskan beberapa parameter atau mereferensikan parameter yang sama beberapa kali dalam kueri, seperti yang ditunjukkan pada kode berikut:

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

@Query(
    """
    SELECT * FROM user
    WHERE first_name LIKE :search OR last_name LIKE :search
    """
)
suspend fun findUserWithName(search: String): List<User>

Meneruskan kumpulan parameter ke kueri

Beberapa fungsi DAO mungkin mengharuskan Anda meneruskan parameter dengan jumlah bervariasi yang tidak diketahui hingga runtime. Jika parameter merepresentasikan kumpulan, parameter akan otomatis diperluas saat runtime berdasarkan jumlah nilai.

Misalnya, kode berikut menentukan fungsi yang menampilkan informasi semua pengguna dari subset region:

@Query("SELECT * FROM user WHERE region IN (:regions)")
suspend fun loadUsersFromRegions(regions: List<String>): List<User>

Membuat kueri beberapa tabel

Beberapa kueri mungkin memerlukan akses ke beberapa tabel untuk menghitung hasilnya. Anda dapat menggunakan klausa JOIN dalam kueri SQL untuk mereferensikan lebih dari satu tabel.

Kode berikut menentukan fungsi yang menggabungkan tiga tabel untuk menampilkan buku yang saat ini dipinjamkan kepada pengguna tertentu:

@Query(
    """
    SELECT * FROM book
    INNER JOIN loan ON loan.book_id = book.id
    INNER JOIN user ON user.id = loan.user_id
    WHERE user.name LIKE :userName
    """
)
suspend fun findBooksBorrowedByName(userName: String): List<Book>

Anda juga dapat menentukan objek data untuk menampilkan subset kolom dari beberapa tabel gabungan. Untuk mengetahui informasi selengkapnya, lihat Menampilkan subset kolom tabel. Kode berikut menentukan DAO dengan fungsi yang menampilkan nama pengguna dan nama buku yang telah dipinjam:

interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user, book
        WHERE user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): Flow<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

Menampilkan multimap

Untuk operasi gabungan, Anda juga dapat membuat kueri kolom dari beberapa tabel tanpa menentukan class data tambahan dengan menulis fungsi kueri yang menampilkan multimap.

Pertimbangkan contoh dari Membuat kueri beberapa tabel. Daripada menampilkan daftar instance dari class data kustom yang menyimpan pasangan instance User dan Book, Anda dapat menampilkan pemetaan User dan Book secara langsung dari fungsi kueri Anda:

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNames(): Map<User, List<Book>>

Saat fungsi kueri menampilkan multimap, Anda dapat menulis kueri yang menggunakan klausa GROUP BY agar dapat memanfaatkan kemampuan SQL untuk penghitungan dan pemfilteran lanjutan. Misalnya, Anda dapat mengubah fungsi loadUserAndBookNames untuk hanya menampilkan pengguna dengan tiga buku atau lebih yang dipinjam:

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    GROUP BY user.name HAVING COUNT(book.id) >= 3
    """
)
suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>

Jika tidak perlu memetakan keseluruhan objek, Anda juga dapat menampilkan pemetaan antara kolom tertentu dalam kueri dengan menggunakan anotasi @MapColumn pada parameter generik jenis yang ditampilkan.

@Query(
    """
    SELECT user.name AS username, book.name AS bookname FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNamesColumns(): Map<
    @MapColumn(columnName = "username") String,
    List<@MapColumn(columnName = "bookname") String>
    >

Jenis nilai yang ditampilkan khusus

Room menyediakan beberapa jenis nilai yang ditampilkan khusus untuk integrasi dengan library API lainnya.

Kueri yang dipaginasi dengan library Paging

Room mendukung kueri yang dipaginasi melalui integrasi dengan the library Paging. Untuk menggunakan jenis nilai yang ditampilkan Paging 3, Anda harus mendaftarkan konverter jenis nilai yang ditampilkan Paging di database atau DAO:

  1. Sertakan artefak androidx.room3:room3-paging dalam konfigurasi build Anda.
  2. Anotasi deklarasi @Database atau @Dao Anda dengan @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).

Setelah terdaftar, DAO Anda dapat menampilkan PagingSource objek untuk digunakan dengan Paging 3:

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM users WHERE label LIKE :query")
    fun pagingSource(query: String): PagingSource<Int, User>
}

Untuk mengetahui informasi selengkapnya tentang memilih parameter jenis untuk PagingSource, lihat Memilih jenis kunci dan nilai.

Akses koneksi database langsung

Jika logika aplikasi Anda memerlukan akses langsung tingkat rendah ke koneksi database, Anda dapat menggunakan connection API Room. Anda dapat memperoleh koneksi menggunakan useReaderConnection untuk operasi hanya baca atau useWriterConnection untuk operasi tulis pada instance RoomDatabase, dan menggunakan usePrepared untuk menjalankan pernyataan:

val result: List<Pair<Long, String>> =
    roomDatabase.useReaderConnection { connection ->
        connection.usePrepared(
            "SELECT * FROM user WHERE age > :minAge LIMIT 5"
        ) { stmt ->
            // Bind arguments if needed
            stmt.bindLong(1, minAge.toLong())
            buildList {
                // Step through the results
                while (stmt.step()) {
                    add(stmt.getLong(0) to stmt.getText(1))
                }
            }
        }
    }

Jika Anda perlu melakukan transaksi database tingkat rendah langsung pada koneksi, Anda dapat menggunakan fungsi helper immediateTransaction, deferredTransaction, atau exclusiveTransaction pada instance Transactor di dalam blok useWriterConnection:

roomDatabase.useWriterConnection { transactor ->
    transactor.immediateTransaction {
        // Perform transactional database operations using transactor
    }
}

Atau, jika Anda hanya perlu menjalankan operasi DAO tingkat tinggi dalam sebuah transaksi, gunakan fungsi ekstensi helper withReadTransaction atau withWriteTransaction pada instance RoomDatabase Anda:

// Perform transactional read operations (DEFERRED transaction)
val userCount = roomDatabase.withReadTransaction {
    userDao.countUsers()
}

// Perform transactional write operations (IMMEDIATE transaction)
roomDatabase.withWriteTransaction {
    userDao.insert(newUser)
    userDao.update(existingUser)
}