Introducción Práctica a Room: El Framework Oficial de Persistencia para Android

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:

  1. Entidad (@Entity): Representa una tabla en la base de datos. Cada campo corresponde a una columna.
  2. DAO (@Dao): Interfaz o clase abstracta que define operaciones de acceso a datos (INSERT, UPDATE, DELETE, SELECT).
  3. 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.

Etiquetas: room Android jetpack SQLite Kotlin

Publicado el 9-8 20:24