Cómo hacer referencia a datos complejos con Room

Room proporciona funciones para convertir entre tipos primitivos y encuadrados, pero no admite referencias de objetos entre entidades. En este documento, se explica cómo usar convertidores de tipos y por qué Room no admite referencias de objetos.

Cómo usar convertidores de tipo

A veces, tu app necesita usar un tipo de datos personalizado cuyo valor deseas almacenar en una sola columna de base de datos. Para agregar este tipo de compatibilidad con los tipos personalizados, debes proporcionar un objeto TypeConverter, que convierte una clase personalizada en un tipo conocido para que Room pueda conservarlo y viceversa.

Por ejemplo, si queremos conservar instancias de Date, podemos escribir el siguiente TypeConverter para almacenar la marca de tiempo de Unix equivalente en la base de datos:

Kotlin

    class Converters {
        @TypeConverter
        fun fromTimestamp(value: Long?): Date? {
            return value?.let { Date(it) }
        }

        @TypeConverter
        fun dateToTimestamp(date: Date?): Long? {
            return date?.time?.toLong()
        }
    }
    

Java

    public class Converters {
        @TypeConverter
        public static Date fromTimestamp(Long value) {
            return value == null ? null : new Date(value);
        }

        @TypeConverter
        public static Long dateToTimestamp(Date date) {
            return date == null ? null : date.getTime();
        }
    }
    

En el ejemplo anterior, se definen 2 funciones: una que convierte un objeto Date en uno Long, y otra que realiza la conversión inversa (de Long a Date). Como Room ya sabe cómo conservar objetos Long, puede usar este convertidor para conservar los valores del tipo Date.

A continuación, debes agregar la anotación @TypeConverters a la clase AppDatabase para que Room pueda usar el convertidor que definiste en cada entidad y DAO en esa clase AppDatabase:

AppDatabase

Kotlin

    @Database(entities = arrayOf(User::class), version = 1)
    @TypeConverters(Converters::class)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }
    

Java

    @Database(entities = {User.class}, version = 1)
    @TypeConverters({Converters.class})
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    

Con estos convertidores, puedes usar tus tipos personalizados en otras consultas, tal como lo harías con los tipos primitivos, como se muestra en el siguiente fragmento de código:

User

Kotlin

    @Entity
    data class User(private val birthday: Date?)
    

Java

    @Entity
    public class User {
        private Date birthday;
    }
    

UserDao

Kotlin

    @Dao
    interface UserDao {
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        fun findUsersBornBetweenDates(from: Date, to: Date): List<User>
    }
    

Java

    @Dao
    public interface UserDao {
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        List<User> findUsersBornBetweenDates(Date from, Date to);
    }
    

También puedes limitar los elementos @TypeConverters a diferentes ámbitos, incluidas entidades individuales, DAO y métodos DAO. Para obtener información detallada, consulta la documentación de referencia de la anotación @TypeConverters.

Por qué Room no permite referencias a objetos

Conclusión clave: Room no permite referencias de objetos entre clases de entidades. En su lugar, debes solicitar explícitamente los datos que necesita tu app.

La asignación de relaciones de una base de datos al modelo de objetos correspondiente es una práctica común y funciona muy bien en el servidor. Incluso cuando el programa carga los campos a medida que se accede a ellos, el servidor sigue funcionando correctamente.

Sin embargo, en el cliente, este tipo de carga diferida no es factible porque, generalmente, ocurre en el subproceso de IU y la consulta de información en el subproceso de IU del disco crea problemas de rendimiento significativos. El subproceso de IU suele tener alrededor de 16 ms para calcular y dibujar el diseño actualizado de una actividad, por lo que, incluso si una consulta solo lleva 5 ms, es probable que tu app se quede sin tiempo para dibujar el marco de trabajo, lo que provocaría fallas visuales notables. La consulta podría tardar más tiempo en completarse si hay una transacción independiente ejecutándose en paralelo o si el dispositivo ejecuta otras tareas que requieren mucha memoria. Sin embargo, si no usas la carga diferida, tu app obtiene más datos de los que necesita, lo que crea problemas de consumo de memoria.

Las asignaciones relacionales de objetos generalmente dejan esta decisión a los desarrolladores con el objetivo de que puedan hacer lo que sea mejor para los casos prácticos de sus apps. Los desarrolladores a menudo deciden compartir el modelo entre su app y la IU. Sin embargo, esta solución no se ajusta bien porque, a medida que la interfaz de usuario cambia con el tiempo, el modelo compartido crea problemas que son difíciles de anticipar y depurar para los desarrolladores.

Por ejemplo, considera una IU que carga una lista de objetos Book, cada uno con un objeto Author. Inicialmente, puedes diseñar tus consultas de modo que utilicen la carga diferida para que las instancias de Book obtenga el autor. La primera obtención del campo author consulta la base de datos. Un tiempo después, notas que también debes mostrar el nombre del autor en la IU de tu app. Puedes acceder a este nombre con facilidad, como se muestra en el siguiente fragmento de código:

Kotlin

    authorNameTextView.text = book.author.name
    

Java

    authorNameTextView.setText(book.getAuthor().getName());
    

Sin embargo, este cambio aparentemente insignificante hace que se consulte la tabla Author en el subproceso principal.

Si consultas la información del autor con anticipación, se hace difícil cambiar cómo se cargan esos datos si ya no los necesitas. Por ejemplo, si la IU de tu app ya no necesita mostrar información de Author, habrá cargado esos datos igualmente sin mostrarlos, provocando una pérdida de valioso espacio de memoria. La eficacia de tu app se degrada aún más si la clase Author hace referencia a otra tabla, como Books.

Para hacer referencia a varias entidades con Room simultáneamente, debes crear un POJO que contenga cada entidad y, luego, escribir una consulta que una las tablas correspondientes. Este modelo bien estructurado, combinado con las sólidas capacidades de validación de consultas de Room, permite que tu app consuma menos recursos durante la carga de datos, mejorando el rendimiento de tu app y la experiencia del usuario.