Se la tua app gestisce quantità non banali di dati strutturati, puoi trarre un grande vantaggio dalla persistenza di questi dati in locale. Il caso d'uso più comune è la memorizzazione nella cache di parti di dati pertinenti, in modo che quando il dispositivo non può accedere alla rete, tu possa comunque sfogliare i contenuti offline.
La libreria di persistenza Room fornisce un livello di astrazione su SQLite per consentirti di accedere al database in modo fluido sfruttando tutta la potenza di SQLite.
Configurare Room 2.x
Per utilizzare Room 2.x nella tua app, aggiungi le seguenti dipendenze al file build.gradle dell'app:
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")
}
Componenti principali
Room ha tre componenti principali:
- Classe di database che contiene il database e funge da punto di accesso principale per la connessione sottostante ai dati persistenti dell'app.
- Entità di dati che rappresentano le tabelle nel database dell'app.
- Oggetti di accesso ai dati (DAO) che forniscono metodi che l'app può utilizzare per eseguire query, aggiornare, inserire ed eliminare dati nel database.
La Figura 1 illustra la relazione tra i diversi componenti di Room.
Esempio di implementazione
// 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()
Definire i dati utilizzando le entità
Ogni entità Room rappresenta una tabella nel database. Definisci ogni entità come una
classe annotata con @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
)
- Nomi di tabelle e colonne personalizzati: per impostazione predefinita, Room utilizza il nome della classe come
nome della tabella e i nomi delle proprietà come nomi delle colonne. Per personalizzarli, utilizza
la
tableNameproprietà in@Entitye l'annotazione@ColumnInfo(name = "..."). - Chiave primaria: per definire una chiave primaria, utilizza
@PrimaryKey. Per le chiavi composite, utilizza laprimaryKeysproprietà di@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Ignorare i campi: per impedire la persistenza dei campi, utilizza
@Ignore.
Convertitori di tipi
A volte, devi archiviare tipi personalizzati, ad esempio Date, in una singola colonna.
Fornisci metodi @TypeConverter per convertire i tipi personalizzati in tipi che
Room può rendere persistenti e viceversa.
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() { ... }
Accedere ai dati utilizzando i DAO
I DAO definiscono i metodi per l'interazione con il database. Annota l'interfaccia o la classe astratta
con @Dao.
Metodi di convenienza
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Inserisci: il metodo di inserimento può restituire un
Longche rappresenta l'ID della riga inserita o unList<Long>contenente gli ID di tutte le righe inserite. - Aggiorna o elimina: il metodo di aggiornamento o eliminazione può restituire un
Intche rappresenta il numero di righe interessate.
Metodi di query
Annota i metodi con @Query per scrivere istruzioni SQL. Room convalida le query in fase di compilazione.
@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>
}
Tipi restituiti multimap
In Room 2.4 e versioni successive, i metodi di query possono restituire direttamente una multimap utilizzando il tipo Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Query DAO asincrone
Per evitare blocchi dell'interfaccia utente, le query di database non possono essere eseguite nel thread principale. Rendi asincrone le query utilizzando una delle seguenti integrazioni:
Coroutine e flusso Kotlin
Richiede la dipendenza 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 con RxJava
Richiede room-rxjava2 o 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 con LiveData e Guava
Richiede room-guava per 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>
}
Definire le relazioni in Room 2.x
Per evitare il caricamento lento nel thread dell'interfaccia utente, non puoi utilizzare riferimenti diretti agli oggetti tra le entità. Definisci invece le relazioni utilizzando classi di dati intermedie
con @Relation.
One-to-one
Ogni utente ha una sola libreria.
@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
Ogni utente può avere molte 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
Le playlist possono contenere molti brani e i brani possono essere presenti in molte playlist. Richiede una tabella di unione.
@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>
)
Relazioni nidificate
Esegui una query sugli utenti, sulle loro playlist e su tutti i brani di queste playlist.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Gestione di database
Questa sezione illustra vari aspetti della gestione del database Room, tra cui le visualizzazioni del database, il precompilamento dei dati e le migrazioni del database.
Visualizzazioni del database
Incapsula una query complessa in una classe annotata con @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() { ... }
Precompilare il database
Compila il database all'inizializzazione da un file di asset o dal file system.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migrazioni
Quando modifichi lo schema, incrementa la versione del database e definisci un
Migration oggetto.
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()
- Migrazioni automatiche: se utilizzi Room 2.4.0 o versioni successive, puoi utilizzare
@AutoMigrationper eseguire automaticamente la migrazione delle modifiche di base dello schema. Per farlo, devi impostareexportSchemasutruenella configurazione del database:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Fallback distruttivo: se la perdita di dati è accettabile quando mancano i percorsi di migrazione, chiama
.fallbackToDestructiveMigrationdurante la creazione del database.
Testare le migrazioni
Per verificare le migrazioni, utilizza MigrationTestHelper dall'artefatto room-testing. Per supportare questa funzionalità, assicurati di esportare gli schemi nella configurazione 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
}
}
Eseguire la migrazione da SQLite a Room
Per eseguire la migrazione dell'app da SQLite a Room, completa i seguenti passaggi:
- Aggiorna le dipendenze per includere Room.
- Annota le classi di modelli con
@Entity,@PrimaryKeye@ColumnInfo. - Crea i DAO per sostituire i metodi di query helper.
- Crea una classe RoomDatabase che faccia riferimento alle entità e ai DAO. Incrementa il numero di versione.
- Definisci un percorso di migrazione vuoto perché lo schema non cambia, ma solo
il framework:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Aggiorna l'istanza per utilizzare
Room.databaseBuildercon il percorso di migrazione.