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.

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.
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.
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.
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.
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<User>)
// Flow emits new data whenever the table changes
@Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
fun observeActiveUsers(): Flow<List<User>>
// 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.
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.
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.
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.
@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 : AutoMigrationSpecLe auto-migrazioni richiedono exportSchema = true per confrontare le versioni. Per rinominare colonne, rinominare tabelle o eliminazioni, è necessario fornire una AutoMigrationSpec con le annotazioni appropriate.
Pronto a superare i tuoi colloqui su Android?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
@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<Post>
)
// 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<List<UserWithPosts>>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.
@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<Tag>
)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.
class UserRepository(private val userDao: UserDao) {
val activeUsers: Flow<List<User>> = 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<List<User>> = 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.
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.
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<String> <-> JSON string
@TypeConverter
fun fromStringList(list: List<String>?): String? =
list?.joinToString(separator = ",")
@TypeConverter
fun toStringList(data: String?): List<String>? =
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 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.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UserRepository(private val userDao: UserDao) {
suspend fun createUser(email: String, displayName: String): Result<Long> {
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<T> 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
@Transactionper 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
MigrationTestHelperprima di rilasciare aggiornamenti
Questi pattern possono essere approfonditi con le domande di colloquio su Room database.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Kotlin Flow vs StateFlow vs SharedFlow: domande da colloquio Android nel 2026
Le domande su Kotlin Flow vs StateFlow vs SharedFlow che gli intervistatori Android pongono nel 2026, con risposte chiare, una tabella comparativa e codice pronto per la produzione.

Kotlin Coroutines per Android: Guida Completa 2026
Guida approfondita alle coroutine Kotlin per lo sviluppo Android: funzioni suspend, scope, dispatcher, Flow e pattern avanzati.

Kotlin 2.3 per Android: Destrutturazione Basata sui Nomi, KMP e Domande da Colloquio 2026
Domande da colloquio su Kotlin 2.3 riguardanti la destrutturazione basata sui nomi, Kotlin Multiplatform, parametri di contesto, coroutine e Flow. Preparazione ai colloqui per sviluppatori Android nel 2026 con esempi di codice reali.