Room Database Android 2026 : Migrations, Relations et Coroutines
Guide complet sur Room Database en 2026 : migrations automatiques et manuelles, relations entre entités, intégration avec Kotlin Coroutines et Flow pour applications Android modernes.

Room Database constitue la solution de persistance recommandée par Google pour les applications Android. En 2026, cette bibliothèque a évolué pour offrir une intégration native avec Kotlin Coroutines et Flow, des migrations automatiques robustes et une gestion avancée des relations entre entités. Ce tutoriel présente les fonctionnalités essentielles de Room pour développer des applications Android performantes et maintenables.
Room fait partie de la suite Android Jetpack et fournit une couche d'abstraction au-dessus de SQLite. La version 2.7+ apporte des améliorations significatives pour les migrations automatiques et le support natif des coroutines.
Configuration de Room Database
L'intégration de Room dans un projet Android moderne nécessite l'ajout des dépendances appropriées et la configuration du plugin KSP (Kotlin Symbol Processing) pour la génération de code.
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
val roomVersion = "2.7.0"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}La configuration du compilateur Room permet d'activer les fonctionnalités avancées comme l'export du schéma pour les migrations.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.generateKotlin", "true")
}Définition des Entités et Relations
Room utilise des annotations pour mapper les classes Kotlin vers les tables SQLite. Les entités représentent les tables de la base de données.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
@Entity(
tableName = "posts",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["user_id"])]
)
data class Post(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "user_id")
val userId: Long,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String
)Pour représenter les relations entre entités, Room propose des classes de données avec l'annotation @Relation.
data class UserWithPosts(
@Embedded
val user: User,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val posts: List<Post>
)
data class PostWithUser(
@Embedded
val post: Post,
@Relation(
parentColumn = "user_id",
entityColumn = "id"
)
val user: User
)Les relations many-to-many nécessitent une table de jonction.
@Entity(tableName = "tags")
data class Tag(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String
)
@Entity(
tableName = "post_tag_cross_ref",
primaryKeys = ["postId", "tagId"]
)
data class PostTagCrossRef(
val postId: Long,
val tagId: Long
)
data class PostWithTags(
@Embedded
val post: Post,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = PostTagCrossRef::class,
parentColumn = "postId",
entityColumn = "tagId"
)
)
val tags: List<Tag>
)Création des DAOs avec Coroutines et Flow
Les Data Access Objects (DAO) définissent les opérations de base de données. Room 2.7 offre un support natif pour les coroutines Kotlin et Flow.
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): Flow<User?>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserByIdOnce(userId: Long): User?
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>): List<Long>
@Update
suspend fun updateUser(user: User): Int
@Delete
suspend fun deleteUser(user: User): Int
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: Long): Int
}
@Dao
interface PostDao {
@Query("SELECT * FROM posts WHERE user_id = :userId ORDER BY id DESC")
fun getPostsByUser(userId: Long): Flow<List<Post>>
@Transaction
@Query("SELECT * FROM posts WHERE id = :postId")
fun getPostWithTags(postId: Long): Flow<PostWithTags?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(post: Post): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPostTagCrossRef(crossRef: PostTagCrossRef)
@Transaction
suspend fun insertPostWithTags(post: Post, tagIds: List<Long>) {
val postId = insertPost(post)
tagIds.forEach { tagId ->
insertPostTagCrossRef(PostTagCrossRef(postId, tagId))
}
}
}Configuration de la Base de Données
La classe abstraite annotée avec @Database représente le point d'entrée principal de Room.
@Database(
entities = [
User::class,
Post::class,
Tag::class,
PostTagCrossRef::class
],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}Migrations de Base de Données
Room propose deux approches pour les migrations : automatiques et manuelles. Les migrations automatiques gèrent les modifications simples du schéma.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// ...
}Pour les modifications complexes nécessitant une spécification explicite, Room fournit AutoMigrationSpec.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 3,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = AppDatabase.Migration2To3::class)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@RenameColumn(tableName = "users", fromColumnName = "full_name", toColumnName = "display_name")
@DeleteColumn(tableName = "users", columnName = "legacy_field")
class Migration2To3 : AutoMigrationSpec
// ...
}Les migrations manuelles offrent un contrôle total pour les transformations de données complexes.
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// Création d'une nouvelle table avec le schéma mis à jour
db.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
display_name TEXT NOT NULL,
email TEXT NOT NULL,
avatar_url TEXT,
created_at INTEGER NOT NULL DEFAULT 0
)
""".trimIndent())
// Migration des données existantes
db.execSQL("""
INSERT INTO users_new (id, display_name, email, created_at)
SELECT id, display_name, email, created_at FROM users
""".trimIndent())
// Suppression de l'ancienne table et renommage
db.execSQL("DROP TABLE users")
db.execSQL("ALTER TABLE users_new RENAME TO users")
// Création des index
db.execSQL("CREATE INDEX index_users_email ON users(email)")
}
}
// Application de la migration
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_3_4)
.build()Intégration avec Repository Pattern
L'architecture recommandée utilise un Repository comme intermédiaire entre les ViewModels et les DAOs.
class UserRepository(
private val userDao: UserDao,
private val postDao: PostDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val allUsers: Flow<List<User>> = userDao.getAllUsers()
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?> {
return userDao.getUserWithPosts(userId)
}
suspend fun createUser(fullName: String, email: String): Result<Long> {
return withContext(ioDispatcher) {
try {
val user = User(fullName = fullName, email = email)
val id = userDao.insertUser(user)
Result.success(id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun createPostWithTags(
userId: Long,
title: String,
content: String,
tagIds: List<Long>
): Result<Long> {
return withContext(ioDispatcher) {
try {
val post = Post(userId = userId, title = title, content = content)
postDao.insertPostWithTags(post, tagIds)
Result.success(post.id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}Utilisation avec ViewModel et StateFlow
L'intégration de Room avec les ViewModels permet une gestion réactive de l'interface utilisateur.
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _selectedUserId = MutableStateFlow<Long?>(null)
val users: StateFlow<List<User>> = repository.allUsers
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val selectedUserWithPosts: StateFlow<UserWithPosts?> = _selectedUserId
.filterNotNull()
.flatMapLatest { userId ->
repository.getUserWithPosts(userId)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
fun selectUser(userId: Long) {
_selectedUserId.value = userId
}
fun createUser(fullName: String, email: String) {
viewModelScope.launch {
repository.createUser(fullName, email)
.onSuccess { id ->
_selectedUserId.value = id
}
.onFailure { error ->
// Gestion de l'erreur
}
}
}
}Tests Unitaires de Room Database
Room fournit des utilitaires pour tester les migrations et les opérations de base de données.
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runTest {
val user = User(fullName = "John Doe", email = "john@example.com")
val id = userDao.insertUser(user)
val retrieved = userDao.getUserByIdOnce(id)
assertThat(retrieved).isNotNull()
assertThat(retrieved?.fullName).isEqualTo("John Doe")
}
@Test
fun flowEmitsUpdates() = runTest {
val user = User(fullName = "Jane Doe", email = "jane@example.com")
userDao.getAllUsers().test {
assertThat(awaitItem()).isEmpty()
userDao.insertUser(user)
val users = awaitItem()
assertThat(users).hasSize(1)
assertThat(users[0].fullName).isEqualTo("Jane Doe")
cancelAndIgnoreRemainingEvents()
}
}
}
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate3To4() {
// Création de la base v3
helper.createDatabase("test_db", 3).apply {
execSQL("INSERT INTO users (id, display_name, email, created_at) VALUES (1, 'Test', 'test@test.com', 0)")
close()
}
// Migration vers v4
val db = helper.runMigrationsAndValidate("test_db", 4, true, MIGRATION_3_4)
// Vérification
val cursor = db.query("SELECT * FROM users WHERE id = 1")
assertThat(cursor.moveToFirst()).isTrue()
assertThat(cursor.getString(cursor.getColumnIndex("display_name"))).isEqualTo("Test")
}
}Prêt à réussir tes entretiens Android ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Questions Fréquentes en Entretien
Lors des entretiens techniques Android, les questions sur Room Database reviennent régulièrement. Les candidats doivent maîtriser les différences entre Flow et suspend pour les requêtes, la gestion des migrations sans perte de données, et l'optimisation des requêtes avec relations.
Les points clés incluent la compréhension du fonctionnement interne de Room avec SQLite, l'utilisation appropriée des indices pour les performances, et la gestion des transactions pour maintenir l'intégrité des données.
Room Database reste un composant fondamental de l'écosystème Android Jetpack. La maîtrise des migrations, des relations entre entités et de l'intégration avec Kotlin Coroutines permet de construire des applications robustes et performantes. L'approche réactive avec Flow simplifie la synchronisation entre la base de données et l'interface utilisateur, tandis que le pattern Repository assure une architecture propre et testable.
Partager
Articles similaires

Kotlin Flow vs StateFlow vs SharedFlow : questions d'entretien Android en 2026
Les questions Kotlin Flow vs StateFlow vs SharedFlow que posent les recruteurs Android en 2026, avec des réponses claires, un tableau comparatif et du code prêt pour la production.

Kotlin Multiplatform 2026 : partager du code Android/iOS
Comment Kotlin Multiplatform partage la logique métier entre Android et iOS en 2026 : configuration Gradle, expect/actual, réseau Ktor, interface Compose Multiplatform et questions d'entretien KMP courantes.

Android 16 en 2026 : Nouvelles APIs, Desktop Mode et Questions d'Entretien
Analyse approfondie des changements d'Android 16 API 36 : edge-to-edge obligatoire, Desktop Mode, ProgressStyle, navigation predictive et questions d'entretien technique pour développeurs Android en 2026.