Si votre application gère des quantités importantes de données structurées, vous pouvez bénéficier d'une persistance locale de ces données. Le cas d'utilisation le plus courant consiste à mettre en cache les éléments de données pertinents de sorte que, lorsque l'appareil ne peut pas accéder au réseau, vous puissiez continuer à parcourir ce contenu hors connexion.
La bibliothèque de persistance Room fournit une couche d'abstraction sur SQLite afin de vous permettre d'accéder à la base de données de manière fluide, tout en exploitant toute la puissance de SQLite.
Configurer Room 2.x
Pour utiliser Room 2.x dans votre application, ajoutez les dépendances suivantes au fichier build.gradle de votre application :
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")
}
Composants principaux
Room comporte trois composants principaux :
- Une classe de base de données qui contient la base de données et sert de point d'accès principal pour la connexion sous-jacente aux données persistantes de votre application.
- Des entités de données qui représentent les tables de la base de données de votre application.
- Des objets d'accès aux données (DAO) qui fournissent des méthodes que votre application peut utiliser pour interroger, mettre à jour, insérer et supprimer des données dans la base de données.
La figure 1 illustre la relation entre les différents composants de Room.
Exemple d'implémentation
// 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()
Définir des données à l'aide d'entités
Chaque entité Room représente une table dans la base de données. Vous définissez chaque entité en tant que
classe annotée avec @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
)
- Noms de tables et de colonnes personnalisés : par défaut, Room utilise le nom de la classe comme
nom de table et les noms de propriétés comme noms de colonnes. Pour les personnaliser, utilisez
la propriété
tableNamedans@Entityet l'annotation@ColumnInfo(name = "..."). - Clé primaire : pour définir une clé primaire, utilisez
@PrimaryKey. Pour les clés composites, utilisez la propriétéprimaryKeysde@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Ignorer des champs : pour empêcher la persistance des champs, utilisez
@Ignore.
Convertisseurs de types
Vous devez parfois stocker des types personnalisés, tels que Date, dans une seule colonne.
Fournissez des méthodes @TypeConverter pour convertir des types personnalisés en types que
Room peut conserver et inversement.
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() { ... }
Accéder aux données à l'aide des DAO
Les DAO définissent des méthodes d'interaction avec la base de données. Annotez l'interface ou la classe abstraite avec @Dao.
Méthodes de base
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Insérer : la méthode d'insertion peut renvoyer un
Longreprésentant l'ID de la ligne insérée, ou uneList<Long>contenant les ID de toutes les lignes insérées. - Mettre à jour ou supprimer : la méthode de mise à jour ou de suppression peut renvoyer un
Intreprésentant le nombre de lignes affectées.
Méthodes de requête
Annotez les méthodes avec @Query pour écrire des instructions SQL. Room valide les requêtes au moment de la compilation.
@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>
}
Types renvoyés en multimap
Dans Room 2.4 et les versions ultérieures, les méthodes de requête peuvent renvoyer directement un multimap à l'aide du type Map :
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Requêtes DAO asynchrones
Pour éviter les blocages de l'interface utilisateur, les requêtes de base de données ne peuvent pas s'exécuter sur le thread principal. Rendez vos requêtes asynchrones à l'aide de l'une des intégrations suivantes :
Coroutines et flux Kotlin
Nécessite la dépendance 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 avec RxJava
Nécessite room-rxjava2 ou 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 avec LiveData et Guava
Nécessite room-guava pour 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>
}
Définir des relations dans Room 2.x
Pour éviter le chargement différé sur le thread UI, vous ne pouvez pas utiliser de références d'objet directes entre les entités. Définissez plutôt des relations à l'aide de classes de données intermédiaires
avec @Relation.
Un à un
Chaque utilisateur ne possède qu'une seule bibliothèque.
@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>
Un à plusieurs
Chaque utilisateur peut avoir plusieurs playlists.
@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>
)
Plusieurs à plusieurs
Les playlists peuvent contenir de nombreux titres, et les titres peuvent figurer dans de nombreuses playlists. Nécessite une table de jonction.
@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>
)
Relations imbriquées
Interrogez les utilisateurs, leurs playlists et tous les titres de ces playlists.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Gestion de bases de données
Cette section aborde différents aspects de la gestion de votre base de données Room, y compris les vues de base de données, le préremplissage des données et les migrations de base de données.
Vues de base de données
Encapsulez une requête complexe dans une classe annotée avec @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() { ... }
Préremplir la base de données
Remplissez la base de données lors de l'initialisation à partir d'un fichier d'asset ou du système de fichiers.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migrations
Lorsque vous modifiez le schéma, incrémentez la version de la base de données et définissez un
Migration objet.
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()
- Migrations automatisées : si vous utilisez Room 2.4.0 ou une version ultérieure, vous pouvez utiliser
@AutoMigrationpour migrer automatiquement les modifications de schéma de base. Pour cela, vous devez définirexportSchemasurtruedans la configuration de votre base de données :@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Rétablissement destructif : si la perte de données est acceptable lorsque les chemins de migration
sont manquants, appelez
.fallbackToDestructiveMigrationlors de la création de la base de données.
Tester les migrations
Pour vérifier les migrations, utilisez MigrationTestHelper à partir de l'artefact room-testing. Pour ce faire, assurez-vous d'exporter les schémas dans votre configuration 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
}
}
Effectuer une migration de SQLite vers Room
Pour migrer votre application de SQLite vers Room, procédez comme suit :
- Mettez à jour les dépendances pour inclure Room.
- Annotez les classes de modèle avec
@Entity,@PrimaryKeyet@ColumnInfo. - Créez des DAO pour remplacer vos méthodes de requête d'assistance.
- Créez une classe RoomDatabase référençant vos entités et vos DAO. Incrémentez le numéro de version.
- Définissez un chemin de migration vide , car seul
le framework change, et non
le schéma :
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Mettez à jour l'instanciation pour utiliser
Room.databaseBuilderavec le chemin de migration.