Quando utilizzi la libreria di persistenza Room per archiviare i dati della tua app, interagisci con i dati archiviati definendo oggetti di accesso ai dati o DAO. Ogni DAO include funzioni che offrono un accesso astratto al database della tua app. In fase di compilazione, Room genera automaticamente le implementazioni dei DAO che definisci.
Utilizzando i DAO per accedere al database della tua app anziché ai generatori di query o alle query dirette, puoi mantenere la separazione delle responsabilità, un principio architettonico fondamentale. I DAO ti consentono anche di simulare l'accesso al database quando testi la tua app.
Anatomia di un DAO
Puoi definire ogni DAO come interfaccia o classe astratta. Per i casi d'uso di base, in genere utilizzi un'interfaccia. In entrambi i casi, devi sempre
annotare i DAO con @Dao. I DAO non hanno proprietà, ma definiscono una o più funzioni per interagire con i dati nel database della tua app.
Il seguente codice è un esempio di DAO che definisce le funzioni per inserire, eliminare e selezionare gli oggetti User in un 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> }
Esistono due tipi di funzioni DAO che definiscono le interazioni con il database:
- Funzioni di praticità che consentono di inserire, aggiornare ed eliminare righe nel database senza scrivere codice SQL.
- Funzioni di query che consentono di scrivere la propria query SQL per interagire con il database.
Le sezioni seguenti mostrano come utilizzare entrambi i tipi di funzioni DAO per definire le interazioni con il database di cui ha bisogno la tua app.
Funzioni di praticità
Room fornisce annotazioni di praticità per definire le funzioni che eseguono inserimenti, aggiornamenti ed eliminazioni senza richiedere la scrittura di un'istruzione SQL.
Se devi definire inserimenti, aggiornamenti o eliminazioni più complessi o se devi eseguire query sui dati nel database, utilizza invece una funzione di query.
Inserisci
L'annotazione @Insert consente di definire le funzioni che inseriscono i relativi
parametri nella tabella appropriata del database. Il seguente codice mostra esempi di funzioni @Insert valide che inseriscono uno o più oggetti User nel 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>) }
Ogni parametro per una funzione @Insert deve essere un'istanza di una classe di entità di dati
Room annotata con @Entity o una raccolta di istanze di classi di entità di dati. Quando viene chiamata una funzione @Insert, Room inserisce ogni istanza di entità passata nella tabella del database corrispondente.
Se la funzione @Insert riceve un singolo parametro, può restituire un valore Long, che è il nuovo rowId per l'elemento inserito. Se il parametro è un array o una raccolta, deve restituire un array o una raccolta di valori Long, con ogni valore come rowId per uno degli elementi inseriti.
Per saperne di più sulla restituzione dei valori rowId, consulta la documentazione di riferimento
per l'annotazione @Insert e la documentazione di SQLite
per le tabelle rowid.
Aggiorna
L'@Update annotazione consente di definire le funzioni che aggiornano righe specifiche
in una tabella di database. Come le funzioni @Insert, le funzioni @Update accettano istanze di entità di dati come parametri. Il seguente codice mostra un esempio di funzione @Update che tenta di aggiornare uno o più oggetti User nel database:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room utilizza la chiave primaria per abbinare le istanze di entità negli argomenti alle righe del database. Se non esiste una riga con la stessa chiave primaria, Room non apporta modifiche.
Una funzione @Update può facoltativamente restituire un valore Int che indica il numero di righe aggiornate correttamente.
Elimina
L'@Delete annotazione consente di
definire le funzioni che eliminano righe specifiche da una tabella di database. Come le funzioni @Insert, le funzioni @Delete accettano istanze di entità di dati come parametri. Il seguente codice mostra un esempio di funzione @Delete che tenta di eliminare uno o più oggetti User dal database:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room utilizza la chiave primaria per abbinare le istanze di entità negli argomenti alle righe del database. Se non esiste una riga con la stessa chiave primaria, Room non apporta modifiche.
Una funzione @Delete può facoltativamente restituire un valore Int che indica il numero di righe eliminate correttamente.
Esegui upsert
L'annotazione @Upsert consente di
definire le funzioni che inseriscono istanze di entità quando non esiste una riga corrispondente o
le aggiornano se esiste già una riga con la stessa chiave primaria.
Come le funzioni @Insert e @Update, le funzioni @Upsert accettano istanze di entità di dati come parametri. Il seguente codice mostra un esempio di funzione @Upsert che tenta di eseguire l'upsert di uno o più oggetti User nel database:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
Se la funzione @Upsert riceve un singolo parametro, può restituire un valore Long. Se viene inserita una nuova riga, restituisce il rowId della riga appena inserita. Se viene aggiornata una riga esistente, restituisce -1. Se il parametro è un array o una raccolta, deve restituire un array o una raccolta di valori Long.
Funzioni di query
L'annotazione @Query consente di
scrivere istruzioni SQL ed esporle come funzioni DAO. Utilizza queste funzioni di query per eseguire query sui dati dal database della tua app o quando devi eseguire inserimenti, aggiornamenti ed eliminazioni più complessi.
Room convalida le query SQL in fase di compilazione. Ciò significa che, se si verifica un problema con la query, si verifica un errore di compilazione anziché un errore di runtime.
Query semplici
Il seguente codice definisce una funzione che utilizza una query SELECT per restituire tutti gli oggetti User nel database:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
Le sezioni seguenti mostrano come modificare questo esempio per i casi d'uso tipici.
Restituisci un sottoinsieme delle colonne di una tabella
La maggior parte delle volte, devi restituire solo un sottoinsieme delle colonne della tabella su cui stai eseguendo la query. Ad esempio, l'interfaccia utente potrebbe mostrare solo il nome e il cognome di un utente anziché tutti i dettagli relativi all'utente. Per risparmiare risorse e semplificare l'esecuzione della query, esegui query solo sulle proprietà di cui hai bisogno.
Room consente di restituire un oggetto dati da una qualsiasi delle query, a condizione che sia possibile mappare l'insieme delle colonne dei risultati sull'oggetto restituito. Ad esempio, puoi definire il seguente oggetto per contenere il nome e il cognome di un utente:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
Poi, puoi restituire l'oggetto dati dalla funzione di query:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
Poiché la query restituisce i valori per le colonne first_name e last_name, Room mappa questi valori sulle proprietà della classe NameTuple. Se la query restituisce una colonna che non esegue il mapping a una proprietà nell'oggetto restituito, Room visualizza un avviso.
Sebbene l'esempio precedente utilizzi una classe di dati personalizzata per recuperare un sottoinsieme di colonne, Room supporta anche la restituzione di kotlin.Pair e kotlin.Triple per praticità quando una query restituisce esattamente due o tre colonne. Quando utilizzi questi tipi, le colonne vengono mappate in base all'ordine in cui sono definite nell'istruzione della query, quindi l'ordine delle colonne nell'istruzione SELECT deve corrispondere all'ordine dei tipi in Pair o Triple.
Passa parametri semplici a una query
La maggior parte delle volte, le funzioni DAO devono accettare parametri in modo da poter eseguire operazioni di filtro. Room supporta l'utilizzo dei parametri delle funzioni come parametri di binding nelle query.
Ad esempio, il seguente codice definisce una funzione che restituisce tutti gli utenti di una determinata età:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
Puoi anche passare più parametri o fare riferimento allo stesso parametro più volte in una query, come illustrato nel seguente codice:
@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>
Passa una raccolta di parametri a una query
Alcune delle tue funzioni DAO potrebbero richiedere di passare un numero variabile di parametri che non è noto fino al runtime. Se un parametro rappresenta una raccolta, viene espanso automaticamente in fase di runtime in base al numero di valori.
Ad esempio, il seguente codice definisce una funzione che restituisce informazioni su tutti gli utenti di un sottoinsieme di regioni:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
Esegui query su più tabelle
Alcune delle tue query potrebbero richiedere l'accesso a più tabelle per calcolare il risultato. Puoi utilizzare le clausole JOIN nelle query SQL per fare riferimento a più di una tabella.
Il seguente codice definisce una funzione che unisce tre tabelle per restituire i libri attualmente in prestito a un utente specifico:
@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>
Puoi anche definire oggetti dati per restituire un sottoinsieme di colonne da più tabelle unite. Per ulteriori informazioni, consulta Restituisci un sottoinsieme delle colonne di una tabella. Il seguente codice definisce un DAO con una funzione che restituisce i nomi degli utenti e i nomi dei libri che hanno preso in prestito:
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)
Restituisci una multimappa
Per le operazioni di unione, puoi anche eseguire query sulle colonne di più tabelle senza definire una classe di dati aggiuntiva scrivendo funzioni di query che restituiscono una multimappa.
Considera l'esempio di Esegui query su più tabelle. Anziché restituire un elenco di istanze di una classe di dati personalizzata che contiene coppie di istanze User e Book, puoi restituire un mapping di User e Book direttamente dalla funzione di query:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
Quando la funzione di query restituisce una multimappa, puoi scrivere query che utilizzano clausole GROUP BY, sfruttando le funzionalità di SQL per calcoli e filtri avanzati. Ad esempio, puoi modificare la funzione loadUserAndBookNames per restituire solo gli utenti con tre o più libri in prestito:
@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>>
Se non devi mappare interi oggetti, puoi anche restituire mapping tra
colonne specifiche nella query utilizzando l'annotazione @MapColumn sui
parametri generici del tipo restituito.
@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> >
Tipi restituiti speciali
Room fornisce alcuni tipi restituiti speciali per l'integrazione con altre librerie API.
Query impaginate con la libreria Paging
Room supporta le query impaginate tramite l'integrazione con la libreria Paging. Per utilizzare i tipi restituiti di Paging 3, devi registrare i convertitori di tipi restituiti di Paging nel database o nel DAO:
- Includi l'artefatto
androidx.room3:room3-pagingnella configurazione della build. - Annota la dichiarazione
@Databaseo@Daocon@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
Una volta registrati, i DAO possono restituire PagingSource oggetti da utilizzare con
Paging 3:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
Per ulteriori informazioni sulla scelta dei parametri di tipo per un PagingSource, consulta
Selezionare i tipi di chiave e valore.
Accesso diretto alla connessione al database
Se la logica della tua app richiede un accesso diretto e di basso livello alla connessione al database, puoi utilizzare le API di connessione di Room. Puoi ottenere una
connessione utilizzando
useReaderConnection
per le operazioni di sola lettura o
useWriterConnection
per le operazioni di scrittura sull'istanza RoomDatabase e utilizzare
usePrepared
per eseguire le istruzioni:
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)) } } } }
Se devi eseguire transazioni di database di basso livello direttamente sulla
connessione, puoi utilizzare le funzioni di assistenza immediateTransaction,
deferredTransaction o exclusiveTransaction su
un'istanza Transactor all'interno di un blocco useWriterConnection:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
In alternativa, se devi eseguire solo operazioni DAO di alto livello in una
transazione, utilizza le withReadTransaction o withWriteTransaction
funzioni di estensione di assistenza sull'istanza RoomDatabase:
// 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) }