# Room Database in Android 2026: Migrazioni, Relazioni e Coroutines > Padroneggia Room database in Android con migrazioni, relazioni tra entità, Kotlin Coroutines e Flow. Tutorial completo con pattern pronti per la produzione per il 2026. - Published: 2026-07-10 - Updated: 2026-07-10 - Author: SharpSkill - Tags: android, room, kotlin, sqlite, coroutines, database - Reading time: 12 min --- Room database fornisce un livello di astrazione robusto sopra SQLite in Android, gestendo il codice boilerplate che rende l'utilizzo diretto di SQLite tedioso. Con Room 2.6 e Kotlin coroutines, la costruzione di layer database reattivi e type-safe è diventata significativamente più semplice. > **Funzionalità Principali di Room 2.6** > > Room 2.6 (stabile nel 2026) introduce supporto migliorato per le migrazioni, elaborazione KSP più veloce e integrazione Flow migliore. Questo tutorial utilizza queste API più recenti per codice pronto per la produzione. ## Configurare Room Database con Kotlin KSP Room richiede tre componenti fondamentali: entities (tabelle), DAOs (data access objects) e la classe database. Il processore di annotazioni KSP genera il codice di implementazione a tempo di compilazione. Le dependencies vengono aggiunte al file `build.gradle.kts` a livello di modulo. KSP ha sostituito KAPT come processore di annotazioni raccomandato dal 2024, offrendo tempi di build più rapidi. ```kotlin // build.gradle.kts (Module :app) plugins { id("com.google.devtools.ksp") version "2.1.0-1.0.29" } dependencies { val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") // Coroutines support ksp("androidx.room:room-compiler:$roomVersion") } ``` L'artifact `room-ktx` fornisce estensioni per coroutines, permettendo funzioni suspend nei DAOs. ## Definire Entities con Primary Keys e Indici Le entities vengono mappate direttamente sulle tabelle SQLite. Ogni classe entity necessita di almeno una primary key. L'aggiunta di indici sulle colonne frequentemente interrogate migliora le prestazioni di lettura a costo di scritture leggermente più lente. ```kotlin // User.kt import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "users", indices = [ Index(value = ["email"], unique = true), // Unique constraint + index Index(value = ["created_at"]) // Index for sorting queries ] ) data class User( @PrimaryKey(autoGenerate = true) val id: Long = 0, val email: String, val displayName: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), val isActive: Boolean = true ) ``` L'annotazione `@ColumnInfo` personalizza il nome della colonna quando il nome della proprietà Kotlin differisce dal nome della colonna database desiderato. ## Costruire DAOs con Coroutines e Flow I DAOs definiscono le operazioni sul database. Room 2.6 supporta tre pattern asincroni: funzioni suspend per operazioni one-shot, `Flow` per stream reattivi e `ListenableFuture` per interoperabilità Java. ```kotlin // UserDao.kt import androidx.room.* import kotlinx.coroutines.flow.Flow @Dao interface UserDao { // One-shot insert - returns the generated ID @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long // Batch insert - efficient for multiple records @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertUsers(users: List) // Flow emits new data whenever the table changes @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC") fun observeActiveUsers(): Flow> // Suspend function for one-shot read @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserById(userId: Long): User? // Update returns the number of affected rows @Update suspend fun updateUser(user: User): Int // Delete by primary key @Delete suspend fun deleteUser(user: User) // Custom delete query @Query("DELETE FROM users WHERE is_active = 0") suspend fun deleteInactiveUsers(): Int } ``` Le query che restituiscono Flow emettono automaticamente risultati aggiornati quando i dati sottostanti cambiano. Questo elimina la logica di refresh manuale nei ViewModels. ## Creare la Classe Room Database La classe database collega entities e DAOs insieme. Un pattern singleton previene istanze multiple del database, che causerebbero memory leak. ```kotlin // AppDatabase.kt import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [User::class, Post::class], version = 1, exportSchema = true // Generates schema JSON for migration validation ) 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() // Only for development .build() INSTANCE = instance instance } } } } ``` Impostare `exportSchema = true` genera file JSON dello schema nella cartella `schemas/`, utili per validare le migrazioni nei test. > **Strategia di Migrazione per la Produzione** > > Non utilizzare mai `fallbackToDestructiveMigration()` in produzione. Questo elimina tutti i dati utente quando lo schema cambia. La sezione successiva copre la gestione corretta delle migrazioni. ## Scrivere Migrazioni Database Sicure Le migrazioni preservano i dati utente quando lo schema cambia tra versioni dell'app. Room valida le migrazioni a runtime confrontando lo schema atteso con il database migrato. Una migrazione definisce le istruzioni SQL necessarie per trasformare il database dalla versione N alla versione N+1. ```kotlin // Migrations.kt import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase // Migration from version 1 to 2: add phone_number column val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT") } } // Migration from version 2 to 3: add posts table with foreign key val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """.trimIndent()) db.execSQL("CREATE INDEX index_posts_user_id ON posts(user_id)") } } // Register migrations in database builder fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() .also { INSTANCE = it } } } ``` Room supporta il concatenamento delle migrazioni. Se un utente aggiorna dalla versione 1 alla versione 3, Room esegue MIGRATION_1_2 e poi MIGRATION_2_3 in sequenza. ## Auto-Migrazioni in Room 2.6 Per modifiche dello schema semplici (aggiunta di colonne, tabelle o indici), Room può generare migrazioni automaticamente. Questo riduce il codice boilerplate per aggiornamenti semplici. ```kotlin // AppDatabase.kt @Database( entities = [User::class, Post::class], version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2), // Adding nullable column AutoMigration( from = 2, to = 3, spec = AutoMigration2To3::class // Complex changes need spec ) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { // DAOs... } // Migration spec for changes that need hints @RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "display_name") class AutoMigration2To3 : AutoMigrationSpec ``` Le auto-migrazioni richiedono `exportSchema = true` per confrontare le versioni. Per rinominare colonne, rinominare tabelle o eliminazioni, è necessario fornire una `AutoMigrationSpec` con le annotazioni appropriate. ## Modellare Relazioni tra Entities con Embedded Objects Room supporta tre pattern di relazione: oggetti embedded, one-to-many e many-to-many. A differenza degli ORM, Room restituisce dati piatti — le relazioni richiedono query esplicite. ### One-to-Many: Utente con Posts Un utente può avere più posts. La relazione viene definita usando l'annotazione `@Relation` in una data class che combina entrambe le entities. ```kotlin // Post.kt @Entity( tableName = "posts", foreignKeys = [ ForeignKey( entity = User::class, parentColumns = ["id"], childColumns = ["user_id"], onDelete = ForeignKey.CASCADE // Delete posts when user deleted ) ], indices = [Index("user_id")] // Required for foreign key performance ) data class Post( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "user_id") val userId: Long, val title: String, val content: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() ) // UserWithPosts.kt - Relation container data class UserWithPosts( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "user_id" ) val posts: List ) // In UserDao.kt @Transaction // Ensures atomic read of user + posts @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserWithPosts(userId: Long): UserWithPosts? @Transaction @Query("SELECT * FROM users WHERE is_active = 1") fun observeActiveUsersWithPosts(): Flow> ``` L'annotazione `@Transaction` previene letture inconsistenti quando la relazione coinvolge internamente più query. ### Many-to-Many: Utenti e Tags Le relazioni many-to-many richiedono una tabella junction (cross-reference) che memorizza coppie di foreign keys. ```kotlin // Tag.kt @Entity(tableName = "tags") data class Tag( @PrimaryKey(autoGenerate = true) val tagId: Long = 0, val name: String ) // UserTagCrossRef.kt - Junction table @Entity( tableName = "user_tag_cross_ref", primaryKeys = ["userId", "tagId"], foreignKeys = [ ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE), ForeignKey(entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE) ] ) data class UserTagCrossRef( val userId: Long, val tagId: Long ) // UserWithTags.kt data class UserWithTags( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "tagId", associateBy = Junction(UserTagCrossRef::class) ) val tags: List ) ``` Il parametro `associateBy` specifica la tabella junction che collega utenti e tags. ## Integrare Room con ViewModel e Repository Un'architettura pulita separa il layer database dalla UI. Il pattern repository astrae le sorgenti dati, mentre ViewModel espone lo stato alla Compose UI. ```kotlin // UserRepository.kt class UserRepository(private val userDao: UserDao) { val activeUsers: Flow> = userDao.observeActiveUsers() suspend fun createUser(email: String, displayName: String): Long { val user = User(email = email, displayName = displayName) return userDao.insertUser(user) } suspend fun deactivateUser(userId: Long) { userDao.getUserById(userId)?.let { user -> userDao.updateUser(user.copy(isActive = false)) } } } // UserViewModel.kt class UserViewModel( private val repository: UserRepository ) : ViewModel() { val users: StateFlow> = repository.activeUsers .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun createUser(email: String, name: String) { viewModelScope.launch { repository.createUser(email, name) // No manual refresh needed - Flow emits automatically } } } ``` L'operatore `stateIn` converte il `Flow` freddo da Room in un `StateFlow` caldo adatto per la collection di Compose. Per maggiori dettagli sui pattern coroutines, consultare la guida su [Kotlin Coroutines](/blog/android/mastering-kotlin-coroutines). > **Testing Room Database** > > Room fornisce un builder per database in-memory per i test: `Room.inMemoryDatabaseBuilder()`. I test vengono eseguiti più velocemente e non persistono dati tra le esecuzioni. Le migrazioni dovrebbero sempre essere testate con `MigrationTestHelper` dall'artifact `room-testing`. ## Type Converters per Tipi Complessi Room memorizza nativamente solo tipi primitivi. I type converters trasformano tipi complessi (date, enum, liste) in formati memorizzabili. ```kotlin // Converters.kt import androidx.room.TypeConverter import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset class Converters { // Instant <-> Long (milliseconds) @TypeConverter fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli() @TypeConverter fun toInstant(millis: Long?): Instant? = millis?.let { Instant.ofEpochMilli(it) } // LocalDate <-> String (ISO format) @TypeConverter fun fromLocalDate(date: LocalDate?): String? = date?.toString() @TypeConverter fun toLocalDate(dateStr: String?): LocalDate? = dateStr?.let { LocalDate.parse(it) } // List <-> JSON string @TypeConverter fun fromStringList(list: List?): String? = list?.joinToString(separator = ",") @TypeConverter fun toStringList(data: String?): List? = data?.split(",")?.filter { it.isNotBlank() } } // Register in database class @Database(...) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() ``` Per la serializzazione JSON di oggetti complessi, si consiglia di utilizzare [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) invece della manipolazione manuale delle stringhe. ## Gestire le Operazioni Database nello Scope Coroutines Le operazioni DAO di Room dovrebbero essere eseguite sul dispatcher IO. Sebbene Room gestisca internamente il threading per le funzioni suspend, la gestione degli errori a livello repository mantiene il codice robusto. ```kotlin // UserRepository.kt with error handling import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class UserRepository(private val userDao: UserDao) { suspend fun createUser(email: String, displayName: String): Result { return withContext(Dispatchers.IO) { try { val id = userDao.insertUser( User(email = email, displayName = displayName) ) Result.success(id) } catch (e: SQLiteConstraintException) { // Unique constraint violation (duplicate email) Result.failure(DuplicateEmailException(email)) } } } suspend fun deleteAllInactive(): Int = withContext(Dispatchers.IO) { userDao.deleteInactiveUsers() } } class DuplicateEmailException(email: String) : Exception("Email already exists: $email") ``` Wrappare i risultati in `Result` permette ai ViewModels di gestire gli errori in modo elegante senza boilerplate try-catch. ## Conclusione Costruire un layer Room database pronto per la produzione coinvolge diverse pratiche chiave: - Utilizzare KSP invece di KAPT per compilazione più veloce con Room 2.6 - Aggiungere indici sulle colonne usate nelle clausole WHERE e nelle condizioni JOIN - Restituire Flow dai DAOs per aggiornamenti UI automatici quando i dati cambiano - Scrivere migrazioni esplicite per modifiche dello schema — mai usare migrazione distruttiva in produzione - Utilizzare `@Transaction` per query di relazione per garantire letture consistenti - Wrappare le operazioni database in Result per una gestione pulita degli errori nei ViewModels - Testare le migrazioni con `MigrationTestHelper` prima di rilasciare aggiornamenti Questi pattern possono essere approfonditi con le [domande di colloquio su Room database](/technologies/android/interview-questions/android-room-database). --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/android/room-database-android-2026-migrations-relations-coroutines