Room es la solución oficial de Google para el acceso a bases de datos SQLite en aplicaciones Android. Proporciona una capa de abstracción sobre las APIs nativas de SQLite, eliminando la necesidad de escribir sentencias SQL crudas y reduciendo errores comunes como nombres de columnas mal escritos o cláusulas faltantes. A diferencia de frameworks de terceros como GreenDAO o ORMLite, Room está profundamente integrado con el ecosistema Android Jetpack y ofrece verificación en tiempo de compilación, soporte para LiveData y Flow, y migraciones controladas.
Estrucutra Básica de Room
Un diseño típico con Room consta de tres componentes principales:
- Entidad (
@Entity): Representa una tabla en la base de datos. Cada campo corresponde a una columna. - DAO (
@Dao): Interfaz o clase abstracta que define operaciones de acceso a datos (INSERT, UPDATE, DELETE, SELECT). - Base de datos (
@Database): Clase abstracta que sirve como contenedor lógico para las entidades y DAOs, y gestiona la instancia de la base de datos.
Configuración del Proyecto
Agregue las dependencias necesarias en el archivo app/build.gradle:
dependencies {
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
kapt 'androidx.room:room-compiler:2.6.1' // Para Kotlin
// Si usa Java:
// annotationProcessor 'androidx.room:room-compiler:2.6.1'
}
Room requiere que el repositorio de Google esté declarado en el bloque repositories del archivo build.gradle del proyecto.
Ejemplo Funcional Paso a Paso
1. Definición de la Entidad
Una clase anotada con @Entity representa una tabla. Se recomienda usar campos public o proveer métodos getter/setter.
@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "title")
val title: String = "",
@ColumnInfo(name = "body")
val body: String = "",
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
Nota: @Ignore excluye un campo de la persistencia; @ColumnInfo permite personalizar nombres de columnas.
2. Creación del DAO
El DAO se declara como una interfaz (recomendado) o clase abstracta. Las operaciones se definen mediante anotaciones específicas o consultas personalizadas con @Query.
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: NoteEntity): Long
@Update
suspend fun update(note: NoteEntity)
@Delete
suspend fun delete(note: NoteEntity)
@Query("SELECT * FROM notes ORDER BY created_at DESC")
fun getAll(): Flow<list>>
@Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%'")
fun search(query: String): Flow<list>>
}</list></list>
Room soporta funciones suspendidas (Kotlin) y devoluciones de tipo Flow, lo que facilita la observación reactiva de cambios sin gestionar hilos manualmente.
3. Configuración de la Base de Datos
La clase base de datos extiende RoomDatabase y debe ser abstracta. Se inicializa una sola vez usando el patrón singleton.
@Database(
entities = [NoteEntity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Para evitar bloqueos en la UI, nunca se debe llamar a métodos de base de datos directamente desde el hilo principal — Room arroja una excepción si se detecta tal uso (a menos que se habilite explícitamente .allowMainThreadQueries(), lo cual no se recomienda).
Características Avanzadas
Claves Compuestas y Índices
Se pueden definir múltiples columnas como clave primaria y añadir índices para mejorar el rendimiento de búsquedas frecuentes:
@Entity(
tableName = "user_profiles",
primaryKeys = ["user_id", "profile_type"],
indices = [
Index(value = ["user_id"], unique = true),
Index(value = ["profile_type", "updated_at"])
]
)
data class UserProfile(
val user_id: Long,
val profile_type: String,
val data: String,
val updated_at: Long
)
Relaciones con Claves Foráneas
Room admite claves foráneas para mantener integridad referencial. Por ejemplo, vincular comentarios a una publicación:
@Entity(
foreignKeys = [
ForeignKey(
entity = PostEntity::class,
parentColumns = ["id"],
childColumns = ["post_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class CommentEntity(
@PrimaryKey val id: Long,
val post_id: Long,
val content: String
)
Con onDelete = CASCADE, al eliminar una publicación también se eliminan sus comentarios asociados.
Objetos Anidados con @Embedded
Permite descomponer un objeto complejo en columnas individuales dentro de la misma tabla:
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val sku: String,
val name: String,
@Embedded(prefix = "price_") val pricing: PriceDetails
)
data class PriceDetails(
val amount: Double,
val currency: String,
val last_updated: Long
)
Esto genera columnas como price_amount, price_currency y price_last_updated en la tabla products.
Migraciones Controladas
Cuando cambia el esquema (por ejemplo, al añadir una columna), se deben definir migraciones explícitas para preservar los datos existentes:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE notes ADD COLUMN archived INTEGER NOT NULL DEFAULT 0")
}
}
// En la construcción de la base de datos:
Room.databaseBuilder(context, AppDatabase::class.java, "note_db")
.addMigrations(MIGRATION_1_2)
.build()
Room valida automáticamente las migraciones durante el desarrollo para evitar errores en producción.
Consulta Personalizada y Optimización
Las anotaciones @Query permiten ejecutar cualquier sentencia SQL válida. Algunos ejemplos útiles:
- Conteo condicional:
@Query("SELECT COUNT(*) FROM notes WHERE archived = 1") - Límite y ordenamiento:
@Query("SELECT * FROM notes ORDER BY created_at DESC LIMIT 10") - Búsqueda por rango de fechas:
@Query("SELECT * FROM notes WHERE created_at BETWEEN :start AND :end") - Actualización condicional:
@Query("UPDATE notes SET archived = 1 WHERE id IN (:ids)")
Room verifica la validez de las consultas en tiempo de compilación, reportando errores si una columna o tabla no existe.
Integración con Arquitectura Moderna
Room funciona de forma nativa con componentes de Jetpack como ViewModel, LiveData y Flow. Usando Flow en los DAOs, los cambios en la base de datos se propagan automáticamente a la UI sin necesidad de observadores manuales o llamadas a notifyDataSetChanged().
Además, al combinarlo con WorkManager o Coroutines, se simplifica la sincronización offline-first y la gestión de tareas asincrónicas relacionadas con datos locales.