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 Android 2026 : Migrations, Relations et Coroutines

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.

build.gradle.kts (Module: app)kotlin
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.

build.gradle.ktskotlin
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.

kotlin
@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.

kotlin
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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
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.

kotlin
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.

kotlin
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.

kotlin
@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